|
43082
|
1576
|
4
|
2026-05-14T12:24:00.875+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778761440875_m2.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preferen...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.12566489,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.37466756,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","depth":4,"bounds":{"left":0.38397607,"top":0.10055866,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.39361703,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.40093085,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.6665558,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"169","depth":4,"bounds":{"left":0.6765292,"top":0.12529927,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.69049203,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"102","depth":4,"bounds":{"left":0.70046544,"top":0.12529927,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.7144282,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.7240692,"top":0.123703115,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.73138297,"top":0.123703115,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
7832912520305006022
|
7035618698753419068
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preferen...
|
43060
|
NULL
|
NULL
|
NULL
|
|
43081
|
1575
|
3
|
2026-05-14T12:23:32.402103+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778761412402_m1.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preferen...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"169","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"102","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
7832912520305006022
|
7035618698753419068
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preferen...
|
43059
|
NULL
|
NULL
|
NULL
|
|
43080
|
1576
|
3
|
2026-05-14T12:23:30.460897+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778761410460_m2.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preferen...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.12566489,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.37466756,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","depth":4,"bounds":{"left":0.38397607,"top":0.10055866,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.39361703,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.40093085,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.6665558,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"169","depth":4,"bounds":{"left":0.6765292,"top":0.12529927,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.69049203,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"102","depth":4,"bounds":{"left":0.70046544,"top":0.12529927,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.7144282,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.7240692,"top":0.123703115,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.73138297,"top":0.123703115,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
7832912520305006022
|
7035618698753419068
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preferen...
|
43060
|
NULL
|
NULL
|
NULL
|
|
43079
|
1575
|
2
|
2026-05-14T12:23:02.031163+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778761382031_m1.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preferen...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"169","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"102","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
7832912520305006022
|
7035618698753419068
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preferen...
|
43059
|
NULL
|
NULL
|
NULL
|
|
43078
|
1576
|
2
|
2026-05-14T12:23:00.035327+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778761380035_m2.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preferen...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.12566489,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.37466756,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","depth":4,"bounds":{"left":0.38397607,"top":0.10055866,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.39361703,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.40093085,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.6665558,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"169","depth":4,"bounds":{"left":0.6765292,"top":0.12529927,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.69049203,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"102","depth":4,"bounds":{"left":0.70046544,"top":0.12529927,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.7144282,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.7240692,"top":0.123703115,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.73138297,"top":0.123703115,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
7832912520305006022
|
7035618698753419068
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preferen...
|
43060
|
NULL
|
NULL
|
NULL
|
|
43077
|
1575
|
1
|
2026-05-14T12:22:31.676489+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778761351676_m1.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preferen...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"169","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"102","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
7832912520305006022
|
7035618698753419068
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preferen...
|
43059
|
NULL
|
NULL
|
NULL
|
|
43076
|
1576
|
1
|
2026-05-14T12:22:29.601783+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778761349601_m2.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preferen...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.12566489,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.37466756,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","depth":4,"bounds":{"left":0.38397607,"top":0.10055866,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.39361703,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.40093085,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.6665558,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"169","depth":4,"bounds":{"left":0.6765292,"top":0.12529927,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.69049203,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"102","depth":4,"bounds":{"left":0.70046544,"top":0.12529927,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.7144282,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.7240692,"top":0.123703115,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.73138297,"top":0.123703115,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
7832912520305006022
|
7035618698753419068
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preferen...
|
43060
|
NULL
|
NULL
|
NULL
|
|
43075
|
1575
|
0
|
2026-05-14T12:22:01.301189+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778761321301_m1.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preferen...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"169","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"102","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
7832912520305006022
|
7035618698753419068
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preferen...
|
43059
|
NULL
|
NULL
|
NULL
|
|
43074
|
1576
|
0
|
2026-05-14T12:21:59.142053+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778761319142_m2.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preferen...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.12566489,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.37466756,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","depth":4,"bounds":{"left":0.38397607,"top":0.10055866,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.39361703,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.40093085,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.6665558,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"169","depth":4,"bounds":{"left":0.6765292,"top":0.12529927,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.69049203,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"102","depth":4,"bounds":{"left":0.70046544,"top":0.12529927,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.7144282,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.7240692,"top":0.123703115,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.73138297,"top":0.123703115,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
7832912520305006022
|
7035618698753419068
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preferen...
|
43060
|
NULL
|
NULL
|
NULL
|
|
43073
|
NULL
|
0
|
2026-05-14T12:21:30.968644+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778761290968_m1.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preferen...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"169","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"102","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
7832912520305006022
|
7035618698753419068
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preferen...
|
43059
|
NULL
|
NULL
|
NULL
|
|
43072
|
NULL
|
0
|
2026-05-14T12:21:28.734086+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778761288734_m2.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preferen...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.12566489,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.37466756,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","depth":4,"bounds":{"left":0.38397607,"top":0.10055866,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.39361703,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.40093085,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.6665558,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"169","depth":4,"bounds":{"left":0.6765292,"top":0.12529927,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.69049203,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"102","depth":4,"bounds":{"left":0.70046544,"top":0.12529927,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.7144282,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.7240692,"top":0.123703115,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.73138297,"top":0.123703115,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
7832912520305006022
|
7035618698753419068
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preferen...
|
43060
|
NULL
|
NULL
|
NULL
|
|
43071
|
1573
|
10
|
2026-05-14T12:21:00.617787+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778761260617_m1.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preferen...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"169","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"102","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
7832912520305006022
|
7035618698753419068
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preferen...
|
43059
|
NULL
|
NULL
|
NULL
|
|
43070
|
1574
|
10
|
2026-05-14T12:20:58.301953+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778761258301_m2.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preferen...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.12566489,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.37466756,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","depth":4,"bounds":{"left":0.38397607,"top":0.10055866,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.39361703,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.40093085,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.6665558,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"169","depth":4,"bounds":{"left":0.6765292,"top":0.12529927,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.69049203,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"102","depth":4,"bounds":{"left":0.70046544,"top":0.12529927,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.7144282,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.7240692,"top":0.123703115,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.73138297,"top":0.123703115,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
7832912520305006022
|
7035618698753419068
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preferen...
|
43060
|
NULL
|
NULL
|
NULL
|
|
43069
|
1573
|
9
|
2026-05-14T12:20:30.235079+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778761230235_m1.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preferen...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"169","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"102","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
7832912520305006022
|
7035618698753419068
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preferen...
|
43059
|
NULL
|
NULL
|
NULL
|
|
43068
|
1574
|
9
|
2026-05-14T12:20:27.904416+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778761227904_m2.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preferen...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.12566489,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.37466756,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","depth":4,"bounds":{"left":0.38397607,"top":0.10055866,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.39361703,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.40093085,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.6665558,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"169","depth":4,"bounds":{"left":0.6765292,"top":0.12529927,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.69049203,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"102","depth":4,"bounds":{"left":0.70046544,"top":0.12529927,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.7144282,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.7240692,"top":0.123703115,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.73138297,"top":0.123703115,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
7832912520305006022
|
7035618698753419068
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preferen...
|
43060
|
NULL
|
NULL
|
NULL
|
|
43067
|
1573
|
8
|
2026-05-14T12:19:59.850142+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778761199850_m1.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preferen...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"169","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"102","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
7832912520305006022
|
7035618698753419068
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preferen...
|
43059
|
NULL
|
NULL
|
NULL
|
|
43066
|
1574
|
8
|
2026-05-14T12:19:57.426907+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778761197426_m2.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preferen...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.12566489,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.37466756,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","depth":4,"bounds":{"left":0.38397607,"top":0.10055866,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.39361703,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.40093085,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.6665558,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"169","depth":4,"bounds":{"left":0.6765292,"top":0.12529927,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.69049203,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"102","depth":4,"bounds":{"left":0.70046544,"top":0.12529927,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.7144282,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.7240692,"top":0.123703115,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.73138297,"top":0.123703115,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
7832912520305006022
|
7035618698753419068
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preferen...
|
43060
|
NULL
|
NULL
|
NULL
|
|
43065
|
1573
|
7
|
2026-05-14T12:19:29.304588+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778761169304_m1.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preferen...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"169","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"102","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
7832912520305006022
|
7035618698753419068
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preferen...
|
43059
|
NULL
|
NULL
|
NULL
|
|
43064
|
1574
|
7
|
2026-05-14T12:18:58.966800+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778761138966_m2.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preferen...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.12566489,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.37466756,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","depth":4,"bounds":{"left":0.38397607,"top":0.10055866,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.39361703,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.40093085,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.6665558,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"169","depth":4,"bounds":{"left":0.6765292,"top":0.12529927,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.69049203,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"102","depth":4,"bounds":{"left":0.70046544,"top":0.12529927,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.7144282,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.7240692,"top":0.123703115,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.73138297,"top":0.123703115,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
7832912520305006022
|
7035618698753419068
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preferen...
|
43060
|
NULL
|
NULL
|
NULL
|
|
43063
|
1573
|
6
|
2026-05-14T12:18:58.949483+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778761138949_m1.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preferen...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"169","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"102","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
7832912520305006022
|
7035618698753419068
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preferen...
|
43059
|
NULL
|
NULL
|
NULL
|
|
43062
|
1574
|
6
|
2026-05-14T12:18:12.184930+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778761092184_m2.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preferen...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.12566489,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.37466756,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","depth":4,"bounds":{"left":0.38397607,"top":0.10055866,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.39361703,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.40093085,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.6665558,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"169","depth":4,"bounds":{"left":0.6765292,"top":0.12529927,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.69049203,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"102","depth":4,"bounds":{"left":0.70046544,"top":0.12529927,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.7144282,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.7240692,"top":0.123703115,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.73138297,"top":0.123703115,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
7832912520305006022
|
7035618698753419068
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preferen...
|
43060
|
NULL
|
NULL
|
NULL
|
|
43061
|
1573
|
5
|
2026-05-14T12:18:09.752972+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778761089752_m1.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preferen...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"169","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"102","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
7832912520305006022
|
7035618698753419068
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preferen...
|
43059
|
NULL
|
NULL
|
NULL
|
|
43060
|
1574
|
5
|
2026-05-14T12:17:41.745055+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778761061745_m2.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preferen...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.12566489,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.37466756,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","depth":4,"bounds":{"left":0.38397607,"top":0.10055866,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.39361703,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.40093085,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.6665558,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"169","depth":4,"bounds":{"left":0.6765292,"top":0.12529927,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.69049203,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"102","depth":4,"bounds":{"left":0.70046544,"top":0.12529927,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.7144282,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.7240692,"top":0.123703115,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.73138297,"top":0.123703115,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
7832912520305006022
|
7035618698753419068
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preferen...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
43059
|
1573
|
4
|
2026-05-14T12:17:39.376938+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778761059376_m1.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preferen...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"169","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"102","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
7832912520305006022
|
7035618698753419068
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preferen...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
43058
|
1574
|
4
|
2026-05-14T12:17:11.286450+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778761031286_m2.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preferen...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.12566489,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.37466756,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","depth":4,"bounds":{"left":0.38397607,"top":0.10055866,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.39361703,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.40093085,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"bounds":{"left":0.120678194,"top":0.0,"width":0.29787233,"height":1.0},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.6665558,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"169","depth":4,"bounds":{"left":0.6765292,"top":0.12529927,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.69049203,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"102","depth":4,"bounds":{"left":0.70046544,"top":0.12529927,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.7144282,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.7240692,"top":0.123703115,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.73138297,"top":0.123703115,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
7832912520305006022
|
7035618698753419068
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preferen...
|
43057
|
NULL
|
NULL
|
NULL
|
|
43057
|
1574
|
3
|
2026-05-14T12:17:07.940654+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778761027940_m2.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhostormVIewINavicarecodeLaravelKeractorWindowFV f PhostormVIewINavicarecodeLaravelKeractorWindowFV faVsco.js( #12077 on JY-20903-update_activity-stage-on...harroledey© SyncFieldAction.php© SyncRelatedActivityMa© WebhookSyncBatchPrcv D IntegrationApp> D Accessors> @ Api.> @ Config.>ODTO• O Filters>@ Jobs• CJ ProspectSearchStrateg› U service Iraitsc) Daraclient.ohoc) Decorareacuivilv.ono© LocalSearch.php1LocalSearchintertace.o© RemoteSearch.phpC) Service,ohov uistenersC) ConvertLeadActivities.rC) PurdeLookunCache.ohr, M Metadatal• M Miaration, M Pioedriven SalestorceM Sieldo• M OnnortunitvMatcher> OpportunitySyncStrater• @ ProspectSearchStrategM ServiceTraits© Client.php© DecorateActivity.phpT DeleteObjectsTrait.php@ sieldDefinitions.phpcPavloacbullder.ono© Profile.php© QueryBuilder.php© QueryHandler.phpC) @uerviterator.php© QuervResults.phpc) Service, ono@ SvncBatchRedisService7 TraitsC Baseclient.ohoC BaseService.ohr(C) CachedCrmServiceDecora(C) CountrvCodeResolver.oho9 CrmActivitvProviderintear(C) CrmOhiectcRecolver nhnyUserinvitationdro.onpUpoateCrmbata.ong© MatchCrmObject.php25.03.25 Ivanovnamespace Jininny services cru20.03.20 Ivano20.03.20 Ivanovuse...Zo.03.zs van26.03.25 Ivanovv class CrmActiv1tyService25.03.25 Ivanov25.03.25 IvanovImportParticipants.php= custom.logscratch &.ison= laravel.logA SF jiminny@localhost]console [PRODItiò accounts [EU]fii stages (EU]iiò teams [EU]ImporbotkecoraingJoo.org© Activity.php xA1v5 AYA console (EU]© FixActivitiesOpportunity.phg1919public tunction constructprivate readonly Teamrepos1torySteamRepos1tory.31.03.25 Ivanovprivate readonly cachedcrmServicedecoratorprivate readonly EmallHelperorivate readonly Resolveleamcrmconnect.zonprivate readonly Loggerinterface1..5Sdecorator,SemailHelper,SteamCrmResolver,Sloader25.03.25 Ivanov31.03.25 Ivanov* Updates CRM data for an activity and its participants.uOTE: This method performs multiple database writes and should be calledClose Annotationsin a transaction by the caller to ensure atomicity.• Show Ditt21 03View21 03= Cooy Revision Numberam Activity $activityam bool SremoteSearch2101El Annotate Revicion2101• Annotate Previous Revision31.0331.03Show in File Historyows ContainerExceptionInterfaceows nocroundexceptzonintertaceows cxcepcion31.03Options• View in hrowser21.03.25 Ivanov31.03.25 Ivanov31.03.25 Ivanov31.03.25vanov31.03.25vanov25.03.25 vanov31.03.25 Ivanovpublic function updateCrmData(Activity sactivity.bool Sremotesearch = false.): void {sscrmservice = nuusparticinants = Sactivity->aetParticivantso:$team = Sactivitv->qetTeamO:25.03.25 Ivanov25.03.25 Ivanov25.03.25 Ivanov25.03.25 Ivanow203520371207825.03.25 Ivanov25.03.25 lvanow$prospectSearchStrategy = ProspectSearchStrateqyFactorv::match(Steam):if (sorospectSearchStrateav->ianoreCrmMatchData00<sthis-sloagen-sinfo(+[ComActivitvServicel Tanonina crm data because of prospect ct, 2089= Sactivitv->aettdol'strategy' => get_class($prospectSearchStrategy),20912092]):¿€ (enomotoSoanch) StryfScrmService = Sthis->teamCrmResolver->resolveForTeam(Steam)=3 entch (CocialAccoun+TokonTnualidEycontion)dChecked out IV.20902-undate activitv-ctade-on-onnortunitv-chande (16 minute& console SlAGiNG84 B169 M4 M 102 24 ^epacam bool senterroom true if participant is entering the room.false if we Just want to update some participa* epacam carbon|null senterlime if senterNow is true then this is the join time when the actual enter has occurrepublic function uodateOrCreateParticinantaarray Sdata.bool SenterRoom = true.?Carbon SenterTime = null,bool SnameMatchina = false.): Participant 1..3Indates narticinant CRM data* @param array(Lead|null,AccouncinuctUpporcunztuinuctcontace nuetstage null,string|null*r Srecords* doaram Participant sparticipant participant the cki data 1s associated wichpublic function updateParticipantCrmData(array Srecords. Participant Sparticipant): voids...}* Jodates activitu CRi data* aparam arrausLeadInull.AccountnutOnnortunitulnull.Contactlnul7Stage|null,strinalnull*} Srecordspublic function updateActivityCrmData(array Srecords): void// Extract the records.[Slead, Saccount, Sopportunity, $contact, $stagel = $records:Cnocolvon - C+hic-sae+lindatoCnmhataPocolvenf).sstrateqy = sresolver->resolveroractivity slead, scontact, saccount)if (Sstrategy == UpdateCrmDataByStrategy::Lead) {I/ Also update the parent activity if required. checking we don't create a mixed lead/account record100% L2. Thu 14 May 15:17:07AskJiminnyReportActivityServiceTest -Pipedrive SDK EvaluaFixing Activity StagImport CRM Activity T+0 ..wSearched MatchCrmObiect::class in appThought for 2s ›searched conterences started in ap.Error during tool executionSearched Conferences, Started in apisedrcned connections.Upened in appSearched event.*new.*Conferences.*Started in appSearched event."new."Participants."Joined in appInought tor isSearched dispatch."Conferences."Started in apoSearched dispatch. Participants. Joined in appSoarched dicnatch *Connections "Onened in anrSearched ConferenceStarted in applhought for 1sBased on my analysis, Activity::updateActivityCrmData() is used in 5 main flowsFlow 1: Calendar Import - Participant ProcessingLocation: TmoortParticinants:.process)) lline 7) When: Durina calendar event imnort when particinants chande or it'sa new activity Trigger•Participant changes detected during import• New activitv beina created from calendar Path: orocessParticinantso → uodateActivitvCrmbata()Clow 2. Calendar Imnort - Wranner MethodlLocation: ImportParticipants::updateActivityCrmData() (line 121) When: After CRM lookup completes, beforeloadina the undate Triaaer: Called from Flow 1's nrocess(") method Path: Wrans the Activitv model's method withFlow 3: Conterence CRM Retreshnts:: refrechGrmData() (line 192) When: After a meetina finishes. when validatinaparticipants and running CRM matching Trigger: ConferenceCrmMatcherJob dispatches this to refresh CRM data Path:ConferenceCrmMatcherJob → refreshCrmData() → participantCrmLookup() → updateActivityCrmData()Flow 4: CRM Activitv Service UpdateLocation: CrmAct ivitvService::undateCrmData() (line 89) WherJed weekly usage quota is exhausted. Purchase extra usage to continue using premium models. Quota resets May 17, 11:00SAN CuTY.Ask anvthina (884-L)« CodeSWE-16WN Windsurf Toams 80-24UTF.8io 4 spaces...
|
NULL
|
-7372966802483824565
|
NULL
|
click
|
ocr
|
NULL
|
PhostormVIewINavicarecodeLaravelKeractorWindowFV f PhostormVIewINavicarecodeLaravelKeractorWindowFV faVsco.js( #12077 on JY-20903-update_activity-stage-on...harroledey© SyncFieldAction.php© SyncRelatedActivityMa© WebhookSyncBatchPrcv D IntegrationApp> D Accessors> @ Api.> @ Config.>ODTO• O Filters>@ Jobs• CJ ProspectSearchStrateg› U service Iraitsc) Daraclient.ohoc) Decorareacuivilv.ono© LocalSearch.php1LocalSearchintertace.o© RemoteSearch.phpC) Service,ohov uistenersC) ConvertLeadActivities.rC) PurdeLookunCache.ohr, M Metadatal• M Miaration, M Pioedriven SalestorceM Sieldo• M OnnortunitvMatcher> OpportunitySyncStrater• @ ProspectSearchStrategM ServiceTraits© Client.php© DecorateActivity.phpT DeleteObjectsTrait.php@ sieldDefinitions.phpcPavloacbullder.ono© Profile.php© QueryBuilder.php© QueryHandler.phpC) @uerviterator.php© QuervResults.phpc) Service, ono@ SvncBatchRedisService7 TraitsC Baseclient.ohoC BaseService.ohr(C) CachedCrmServiceDecora(C) CountrvCodeResolver.oho9 CrmActivitvProviderintear(C) CrmOhiectcRecolver nhnyUserinvitationdro.onpUpoateCrmbata.ong© MatchCrmObject.php25.03.25 Ivanovnamespace Jininny services cru20.03.20 Ivano20.03.20 Ivanovuse...Zo.03.zs van26.03.25 Ivanovv class CrmActiv1tyService25.03.25 Ivanov25.03.25 IvanovImportParticipants.php= custom.logscratch &.ison= laravel.logA SF jiminny@localhost]console [PRODItiò accounts [EU]fii stages (EU]iiò teams [EU]ImporbotkecoraingJoo.org© Activity.php xA1v5 AYA console (EU]© FixActivitiesOpportunity.phg1919public tunction constructprivate readonly Teamrepos1torySteamRepos1tory.31.03.25 Ivanovprivate readonly cachedcrmServicedecoratorprivate readonly EmallHelperorivate readonly Resolveleamcrmconnect.zonprivate readonly Loggerinterface1..5Sdecorator,SemailHelper,SteamCrmResolver,Sloader25.03.25 Ivanov31.03.25 Ivanov* Updates CRM data for an activity and its participants.uOTE: This method performs multiple database writes and should be calledClose Annotationsin a transaction by the caller to ensure atomicity.• Show Ditt21 03View21 03= Cooy Revision Numberam Activity $activityam bool SremoteSearch2101El Annotate Revicion2101• Annotate Previous Revision31.0331.03Show in File Historyows ContainerExceptionInterfaceows nocroundexceptzonintertaceows cxcepcion31.03Options• View in hrowser21.03.25 Ivanov31.03.25 Ivanov31.03.25 Ivanov31.03.25vanov31.03.25vanov25.03.25 vanov31.03.25 Ivanovpublic function updateCrmData(Activity sactivity.bool Sremotesearch = false.): void {sscrmservice = nuusparticinants = Sactivity->aetParticivantso:$team = Sactivitv->qetTeamO:25.03.25 Ivanov25.03.25 Ivanov25.03.25 Ivanov25.03.25 Ivanow203520371207825.03.25 Ivanov25.03.25 lvanow$prospectSearchStrategy = ProspectSearchStrateqyFactorv::match(Steam):if (sorospectSearchStrateav->ianoreCrmMatchData00<sthis-sloagen-sinfo(+[ComActivitvServicel Tanonina crm data because of prospect ct, 2089= Sactivitv->aettdol'strategy' => get_class($prospectSearchStrategy),20912092]):¿€ (enomotoSoanch) StryfScrmService = Sthis->teamCrmResolver->resolveForTeam(Steam)=3 entch (CocialAccoun+TokonTnualidEycontion)dChecked out IV.20902-undate activitv-ctade-on-onnortunitv-chande (16 minute& console SlAGiNG84 B169 M4 M 102 24 ^epacam bool senterroom true if participant is entering the room.false if we Just want to update some participa* epacam carbon|null senterlime if senterNow is true then this is the join time when the actual enter has occurrepublic function uodateOrCreateParticinantaarray Sdata.bool SenterRoom = true.?Carbon SenterTime = null,bool SnameMatchina = false.): Participant 1..3Indates narticinant CRM data* @param array(Lead|null,AccouncinuctUpporcunztuinuctcontace nuetstage null,string|null*r Srecords* doaram Participant sparticipant participant the cki data 1s associated wichpublic function updateParticipantCrmData(array Srecords. Participant Sparticipant): voids...}* Jodates activitu CRi data* aparam arrausLeadInull.AccountnutOnnortunitulnull.Contactlnul7Stage|null,strinalnull*} Srecordspublic function updateActivityCrmData(array Srecords): void// Extract the records.[Slead, Saccount, Sopportunity, $contact, $stagel = $records:Cnocolvon - C+hic-sae+lindatoCnmhataPocolvenf).sstrateqy = sresolver->resolveroractivity slead, scontact, saccount)if (Sstrategy == UpdateCrmDataByStrategy::Lead) {I/ Also update the parent activity if required. checking we don't create a mixed lead/account record100% L2. Thu 14 May 15:17:07AskJiminnyReportActivityServiceTest -Pipedrive SDK EvaluaFixing Activity StagImport CRM Activity T+0 ..wSearched MatchCrmObiect::class in appThought for 2s ›searched conterences started in ap.Error during tool executionSearched Conferences, Started in apisedrcned connections.Upened in appSearched event.*new.*Conferences.*Started in appSearched event."new."Participants."Joined in appInought tor isSearched dispatch."Conferences."Started in apoSearched dispatch. Participants. Joined in appSoarched dicnatch *Connections "Onened in anrSearched ConferenceStarted in applhought for 1sBased on my analysis, Activity::updateActivityCrmData() is used in 5 main flowsFlow 1: Calendar Import - Participant ProcessingLocation: TmoortParticinants:.process)) lline 7) When: Durina calendar event imnort when particinants chande or it'sa new activity Trigger•Participant changes detected during import• New activitv beina created from calendar Path: orocessParticinantso → uodateActivitvCrmbata()Clow 2. Calendar Imnort - Wranner MethodlLocation: ImportParticipants::updateActivityCrmData() (line 121) When: After CRM lookup completes, beforeloadina the undate Triaaer: Called from Flow 1's nrocess(") method Path: Wrans the Activitv model's method withFlow 3: Conterence CRM Retreshnts:: refrechGrmData() (line 192) When: After a meetina finishes. when validatinaparticipants and running CRM matching Trigger: ConferenceCrmMatcherJob dispatches this to refresh CRM data Path:ConferenceCrmMatcherJob → refreshCrmData() → participantCrmLookup() → updateActivityCrmData()Flow 4: CRM Activitv Service UpdateLocation: CrmAct ivitvService::undateCrmData() (line 89) WherJed weekly usage quota is exhausted. Purchase extra usage to continue using premium models. Quota resets May 17, 11:00SAN CuTY.Ask anvthina (884-L)« CodeSWE-16WN Windsurf Toams 80-24UTF.8io 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
43056
|
1573
|
3
|
2026-05-14T12:17:07.970539+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778761027970_m1.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
SlackFileEditViewGoHistoryWindowHomeDMsActivityFil SlackFileEditViewGoHistoryWindowHomeDMsActivityFilesLaterMoreJiminny ...# general# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi.Direct messagesP. Galya Dimitrova5. Vasil VasilevR. Aneliya AngelovaStefka StoyanovaTodor StamatovMario Georgiev. Nikolay Ivanovdo James Graham2. Stoyan TanevSteliyan Georgiev&. Petko KashinskiLukas Kovalik y...AppsToastJira CloudGoogle Cale... &sHelp$ 0lhl→Describe what you are looking forVasil VasilevMessagesAdd canvasO Files& Pins +ние имаме някаква история на сделката, имаме стейдж по време наToday ~на активититоно тоя стейдж го ползваме само за това тьрсене в on demand (edited)преди малко подхвърлих иначе на Галя идеята, дали да не ги покажем смените на стейджовете на сделките в deal insightsимаме дата на отваряне и затваряне на сделказащо да не сложим и един маркер кога се е променил стейджатака в таймлайна ще се вижда "развитието" на тая сделкаLukas Kovalik 2:45 PMхм ами то дали да го няма вечеVasil Vasilev 2:45 PMняма гоLukas Kovalik 2:46 PMняма дано ВЕ си го врьщас тази цел точноVasil Vasilev 2:47 PMзнам, проверих, преди да предложа на Галя да ги покажемвЪЗМоЖНоно явно не е направеноVasil Vasilev 3:04 PMв крайна сметка тоя метод Activity:updateActivityCrmData се вика само при подмяна на opportunity, нали така?Lukas Kovalik 3:05 PMгледам го сегаMessage Vasil Vasilev+100% <8• Thu 14 May 15:17:07...
|
NULL
|
-3945735861210841681
|
NULL
|
click
|
ocr
|
NULL
|
SlackFileEditViewGoHistoryWindowHomeDMsActivityFil SlackFileEditViewGoHistoryWindowHomeDMsActivityFilesLaterMoreJiminny ...# general# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi.Direct messagesP. Galya Dimitrova5. Vasil VasilevR. Aneliya AngelovaStefka StoyanovaTodor StamatovMario Georgiev. Nikolay Ivanovdo James Graham2. Stoyan TanevSteliyan Georgiev&. Petko KashinskiLukas Kovalik y...AppsToastJira CloudGoogle Cale... &sHelp$ 0lhl→Describe what you are looking forVasil VasilevMessagesAdd canvasO Files& Pins +ние имаме някаква история на сделката, имаме стейдж по време наToday ~на активититоно тоя стейдж го ползваме само за това тьрсене в on demand (edited)преди малко подхвърлих иначе на Галя идеята, дали да не ги покажем смените на стейджовете на сделките в deal insightsимаме дата на отваряне и затваряне на сделказащо да не сложим и един маркер кога се е променил стейджатака в таймлайна ще се вижда "развитието" на тая сделкаLukas Kovalik 2:45 PMхм ами то дали да го няма вечеVasil Vasilev 2:45 PMняма гоLukas Kovalik 2:46 PMняма дано ВЕ си го врьщас тази цел точноVasil Vasilev 2:47 PMзнам, проверих, преди да предложа на Галя да ги покажемвЪЗМоЖНоно явно не е направеноVasil Vasilev 3:04 PMв крайна сметка тоя метод Activity:updateActivityCrmData се вика само при подмяна на opportunity, нали така?Lukas Kovalik 3:05 PMгледам го сегаMessage Vasil Vasilev+100% <8• Thu 14 May 15:17:07...
|
43054
|
NULL
|
NULL
|
NULL
|
|
43055
|
1574
|
2
|
2026-05-14T12:17:07.076090+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778761027076_m2.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.12566489,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.37466756,"top":0.10055866,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","depth":4,"bounds":{"left":0.38397607,"top":0.10055866,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.39361703,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.40093085,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"bounds":{"left":0.16289894,"top":0.027134877,"width":0.29787233,"height":0.9728651},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false}]...
|
-2373754413741859321
|
840996914513197278
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}...
|
43053
|
NULL
|
NULL
|
NULL
|
|
43054
|
1573
|
2
|
2026-05-14T12:17:07.238419+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778761027238_m1.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"169","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"102","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-902677691708284349
|
840433947375712478
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
43053
|
1574
|
1
|
2026-05-14T12:17:00.476110+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778761020476_m2.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.12566489,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
2680889254368551074
|
-8132091307416769596
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
PhostormVIewINavicarecodeLaravelKeractorWindowFV faVsco.js( #12077 on JY-20903-update_activity-stage-on...haroledeyyUserinvitationdro.onpImportParticipants.php© SyncFieldAction.phpUpoateCrmbata.ong© SyncRelatedActivityManamespace Jiminny\Services\Crm11v5 AVC) WebhookSvncBatchPrcv D IntegrationApp> D Accessors> @ Api.> @ Config.>ODTO• O Filters>@ Jobs• CJ ProspectSearchStrateguse ...v class Crmactiv1tyService• U service I raitsc) Daraclient.ohoc) Decorareacuivilv.onoC) LocalSearch.ohp1LocalSearchintertace.oC) RemoteSearch.ohoC) Service,ohov uistenersC) ConvertLeadActivities.rC) PurdeLookunCache.ohr, M Metadatal• M Miaration, M Pioedriven SalestorceM Sieldo• M OnnortunitvMatcher> OpportunitySyncStrater• @ ProspectSearchStrategM ServiceTraits(e) Client.php© DecorateActivity.phpT DeleteObjectsTrait.php@ cieldDefinitions.phpcPavloacbullder.ono© Profile.php© QueryBuilder.php© QueryHandler.phpC) @uerviterator.php© QuervResults.phpc) Service, ono@ SvncBatchRedisServiceM TraitsC Baseclient.ohoC BaseService.ohr(C) CachedCrmServiceDecora(C) CountrvCodeResolver.oho9 CrmActivitvProviderintear0) CrmActivitvService nhnC) CrmConfiauration Settinas!(C) CrmOhiectcRecolver nhnpublic function constructSteamRepos1tory.private readonly cachedcrmservicedecoratonSdecorator,orivate readonly Resolvereamcrmconnect.ionSteamcrmResoLver.private readonly LoggerInterfaceSloager.* Uodates CRM data for an activitu and its porticioonts.• NOTE: This method performs multiple database writes and should be called• within a transaction by the caller to ensure atomicity.• Qnaram Activitu Sactivitu* @param bool SremoteSearch* acmows concalnerexceptzontnterrace* athrows NotFoundExceptionInterface* Athrows Exception13 usagespublic function updateCrmData(Activity sactivity.bool Sremotesearch = talse)• void {scrmservice = nuuuSoarticinants = sactivitv->oetParticioantso:Steam = Sactivitv->aetteamorSorospectSearchStrateav = ProsoectSearchStrateavFactorv:match(steam):if SorosoectSearchStrateav->ianorecrmMatchDataoDSthis-sloggen-sinfo('[CrmActivitvServicel Tanonina eom data because of nrosnect strateav!. "l= Sactivitv->aetTdou"strateavl => aet class(Snrosnec+SearchStrateav)lI):i4 (GnomotoGoanch)dtryfScrmService = Sthis->teamCrmResolver->resolveForTeam($team):3cstch(CocialAccoun+TokenTnualidGycontion)SChecked out IV.20902-undate activitv-ctade-on-onnortunitv-chande (16 minutes aao)= custom.logscratch &.ison= laravel.logA SF jiminny@localhost]& console [PROD]A console (EU]tiò accounts [EU]fii stages (EU]iiò teams [EU]ImporbotkecoraingJoo.org© Activity.php x© FixActivitiesOpportunity.phg& console SlAGiNGclass Accvicy excenas model Imolemencs84 B169 M4 M 102 24 ^2091Z0Y4epacam bool senterroom true if participant is entering the room.false if we just want to update some participa* Idoacamcarbon nuu enterinme t entertows toue then this s the nonn tome when the actua enter has occunneoublic function uodate0rcreateParticioantaarray $data,bool SenterRoom = truel?Carbon SenterTime = null,bool SnameMatchina = falsel): Participant t...}* lndates narticinant CRM datal* @param arrayfAccount/nucl,Opportunity|null,Contactlnull,stage nullstring|null*r Srecords* doaram Participant sparticipant participant the cki data 1s associated withpublic function updateParticipantCrmData(array Srecords. Participant Sparticipant): voids...?* Uodates activitu CRi data* Aoaram acrausOnnortunitulnull.Contact/null.Stage|null,strinalnull* Srecordspublic function updateActivityCrmData(array Srecords): void// Extract the records.[Slead, Saccount, Sopportunity, $contact, $stagel = $records:Gnocolvon - Cthic-sao+lindateCnmhataPocolvon/).Sstrategy = Sresolver->resolveForActivity(Slead, $contact, $account):if (Sstrategy == UpdateCrmDataByStrategy::Lead) {II Also update the parent activity if required. checking we don't create a mixed lead/account record.100% L2. Thu 14 May 15:17:00AskJiminnyReportActivityServiceTest vCascadePipedrive SDK EvaluaFixing Activity StagImport CRM Activity T+0 ..Lets focus on @Activity•phpfL2091-2163 . WHen is it being used? Give me each flow and also when doe sitSearched updateActivityCrmData in appThought for 1s >Read ImportParticipants.php and 3 other files›Thoucht for 3s )Searched class.*Listener in app/Listeners/Activities/CrmThought for 1s >Searched Matchcrmobiect::class in appError during tool executionSearched Conferences Started in aodSearched Connections).Opened in appSearched event. new. Conterences. Started in appSearched event "new*Particioants."Joined in apo.Inougntror is >Searched dispatch. Participants. Joined in appSearched dispatch.*Connections *Onened in aodSoarched ConferenceStarted in annJed weekly usage quota is exhausted. Purchase extra usage to continue using premium models. Quota resets May 17, 11:00" AN cuTи.Ask anvthina (84L)SWE-16WN Windsurf Teams80-24UTF.8io 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
43052
|
1573
|
1
|
2026-05-14T12:17:00.487955+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778761020487_m1.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
5705633564957252526
|
-8132091238662689850
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
SlackFileEditViewGoHistoryWindowHomeDMsActivityFilesLaterMoreJiminny ...# general# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi.Direct messagesP. Galya Dimitrova5. Vasil VasilevR. Aneliya AngelovaStefka StoyanovaTodor StamatovMario Georgiev. Nikolay Ivanovdo James Graham2. Stoyan TanevSteliyan Georgiev&. Petko KashinskiLukas Kovalik y...AppsToastJira CloudGoogle Cale... &sHelp$ 0lhl→Describe what you are looking forVasil VasilevMessagesAdd canvasO Files& Pins +ние имаме някаква история на сделката, имаме стейдж по време наToday ~на активититоно тоя стейдж го ползваме само за това тьрсене в on demand (edited)преди малко подхвърлих иначе на Галя идеята, дали да не ги покажем смените на стейджовете на сделките в deal insightsимаме дата на отваряне и затваряне на сделказащо да не сложим и един маркер кога се е променил стейджатака в таймлайна ще се вижда "развитието" на тая сделкаLukas Kovalik 2:45 PMхм ами то дали да го няма вечеVasil Vasilev 2:45 PMняма гоLukas Kovalik 2:46 PMняма дано ВЕ си го врьщас тази цел точноVasil Vasilev 2:47 PMзнам, проверих, преди да предложа на Галя да ги покажемвЪЗМоЖНоно явно не е направеноVasil Vasilev 3:04 PMв крайна сметка тоя метод Activity:updateActivityCrmData се вика само при подмяна на opportunity, нали така?Lukas Kovalik 3:05 PMгледам го сегаMessage Vasil Vasilev+100% <8• Thu 14 May 15:17:00...
|
43050
|
NULL
|
NULL
|
NULL
|
|
43050
|
1573
|
0
|
2026-05-14T12:16:48.088297+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778761008088_m1.jpg...
|
PhpStorm
|
faVsco.js – UpdateCrmData.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
SlackFileEditViewGoHistoryWindowHomeDMsActivityFil SlackFileEditViewGoHistoryWindowHomeDMsActivityFilesLaterMoreJiminny ...# general# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi.Direct messagesP. Galya Dimitrova5. Vasil VasilevR. Aneliya AngelovaStefka StoyanovaTodor StamatovMario Georgiev. Nikolay Ivanovdo James Graham2. Stoyan TanevSteliyan Georgiev&. Petko KashinskiLukas Kovalik y...AppsToastJira CloudGoogle Cale... &sHelp$ 0lhl→Describe what you are looking forVasil VasilevMessagesAdd canvasO Files& Pins +ние имаме някаква история на сделката, имаме стейдж по време наToday ~на активититоно тоя стейдж го ползваме само за това тьрсене в on demand (edited)преди малко подхвърлих иначе на Галя идеята, дали да не ги покажем смените на стейджовете на сделките в deal insightsимаме дата на отваряне и затваряне на сделказащо да не сложим и един маркер кога се е променил стейджатака в таймлайна ще се вижда "развитието" на тая сделкаLukas Kovalik 2:45 PMхм ами то дали да го няма вечеVasil Vasilev 2:45 PMняма гоLukas Kovalik 2:46 PMняма дано ВЕ си го врьщас тази цел точноVasil Vasilev 2:47 PMзнам, проверих, преди да предложа на Галя да ги покажемвЪЗМоЖНоно явно не е направеноVasil Vasilev 3:04 PMв крайна сметка тоя метод Activity:updateActivityCrmData се вика само при подмяна на opportunity, нали така?Lukas Kovalik 3:05 PMгледам го сегаMessage Vasil Vasilev+100% <8• Thu 14 May 15:16:47...
|
NULL
|
8551540873961465218
|
NULL
|
click
|
ocr
|
NULL
|
SlackFileEditViewGoHistoryWindowHomeDMsActivityFil SlackFileEditViewGoHistoryWindowHomeDMsActivityFilesLaterMoreJiminny ...# general# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi.Direct messagesP. Galya Dimitrova5. Vasil VasilevR. Aneliya AngelovaStefka StoyanovaTodor StamatovMario Georgiev. Nikolay Ivanovdo James Graham2. Stoyan TanevSteliyan Georgiev&. Petko KashinskiLukas Kovalik y...AppsToastJira CloudGoogle Cale... &sHelp$ 0lhl→Describe what you are looking forVasil VasilevMessagesAdd canvasO Files& Pins +ние имаме някаква история на сделката, имаме стейдж по време наToday ~на активититоно тоя стейдж го ползваме само за това тьрсене в on demand (edited)преди малко подхвърлих иначе на Галя идеята, дали да не ги покажем смените на стейджовете на сделките в deal insightsимаме дата на отваряне и затваряне на сделказащо да не сложим и един маркер кога се е променил стейджатака в таймлайна ще се вижда "развитието" на тая сделкаLukas Kovalik 2:45 PMхм ами то дали да го няма вечеVasil Vasilev 2:45 PMняма гоLukas Kovalik 2:46 PMняма дано ВЕ си го врьщас тази цел точноVasil Vasilev 2:47 PMзнам, проверих, преди да предложа на Галя да ги покажемвЪЗМоЖНоно явно не е направеноVasil Vasilev 3:04 PMв крайна сметка тоя метод Activity:updateActivityCrmData се вика само при подмяна на opportunity, нали така?Lukas Kovalik 3:05 PMгледам го сегаMessage Vasil Vasilev+100% <8• Thu 14 May 15:16:47...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
43049
|
NULL
|
0
|
2026-05-14T12:16:40.765340+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778761000765_m1.jpg...
|
PhpStorm
|
faVsco.js – Activity.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
SlackFileEditViewGoHistoryWindowHomeDMsActivityFil SlackFileEditViewGoHistoryWindowHomeDMsActivityFilesLaterMoreJiminny ...# general# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi.Direct messagesP. Galya Dimitrova5. Vasil VasilevR. Aneliya AngelovaStefka StoyanovaTodor StamatovMario Georgiev. Nikolay Ivanovdo James Graham2. Stoyan TanevSteliyan Georgiev&. Petko KashinskiLukas Kovalik y...AppsToastJira CloudGoogle Cale... &sHelp$ 0lhl→Describe what you are looking forVasil VasilevMessagesAdd canvasO Files& Pins +ние имаме някаква история на сделката, имаме стейдж по време наToday ~на активититоно тоя стейдж го ползваме само за това тьрсене в on demand (edited)преди малко подхвърлих иначе на Галя идеята, дали да не ги покажем смените на стейджовете на сделките в deal insightsимаме дата на отваряне и затваряне на сделказащо да не сложим и един маркер кога се е променил стейджатака в таймлайна ще се вижда "развитието" на тая сделкаLukas Kovalik 2:45 PMхм ами то дали да го няма вечеVasil Vasilev 2:45 PMняма гоLukas Kovalik 2:46 PMняма дано ВЕ си го врьщас тази цел точноVasil Vasilev 2:47 PMзнам, проверих, преди да предложа на Галя да ги покажемвЪЗМоЖНоно явно не е направеноVasil Vasilev 3:04 PMв крайна сметка тоя метод Activity:updateActivityCrmData се вика само при подмяна на opportunity, нали така?Lukas Kovalik 3:05 PMгледам го сегаMessage Vasil Vasilev+100% <8• Thu 14 May 15:16:40...
|
NULL
|
-1633758280007838351
|
NULL
|
click
|
ocr
|
NULL
|
SlackFileEditViewGoHistoryWindowHomeDMsActivityFil SlackFileEditViewGoHistoryWindowHomeDMsActivityFilesLaterMoreJiminny ...# general# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi.Direct messagesP. Galya Dimitrova5. Vasil VasilevR. Aneliya AngelovaStefka StoyanovaTodor StamatovMario Georgiev. Nikolay Ivanovdo James Graham2. Stoyan TanevSteliyan Georgiev&. Petko KashinskiLukas Kovalik y...AppsToastJira CloudGoogle Cale... &sHelp$ 0lhl→Describe what you are looking forVasil VasilevMessagesAdd canvasO Files& Pins +ние имаме някаква история на сделката, имаме стейдж по време наToday ~на активититоно тоя стейдж го ползваме само за това тьрсене в on demand (edited)преди малко подхвърлих иначе на Галя идеята, дали да не ги покажем смените на стейджовете на сделките в deal insightsимаме дата на отваряне и затваряне на сделказащо да не сложим и един маркер кога се е променил стейджатака в таймлайна ще се вижда "развитието" на тая сделкаLukas Kovalik 2:45 PMхм ами то дали да го няма вечеVasil Vasilev 2:45 PMняма гоLukas Kovalik 2:46 PMняма дано ВЕ си го врьщас тази цел точноVasil Vasilev 2:47 PMзнам, проверих, преди да предложа на Галя да ги покажемвЪЗМоЖНоно явно не е направеноVasil Vasilev 3:04 PMв крайна сметка тоя метод Activity:updateActivityCrmData се вика само при подмяна на opportunity, нали така?Lukas Kovalik 3:05 PMгледам го сегаMessage Vasil Vasilev+100% <8• Thu 14 May 15:16:40...
|
43046
|
NULL
|
NULL
|
NULL
|
|
43048
|
NULL
|
0
|
2026-05-14T12:16:40.744451+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778761000744_m2.jpg...
|
PhpStorm
|
faVsco.js – Activity.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhostormVIewINavicarecodeLaravelKeractorWindowFV f PhostormVIewINavicarecodeLaravelKeractorWindowFV faVsco.js( #12077 on JY-20903-update_activity-stage-on...haroledeyUserinvitationbtU.onp© ImportParticipants.php© SyncPlanhat.php© UpdateCrmData.php X© MatchCrmObject.php XO syncuserrllot.onp© ValidateSendingMessage.psiphow2V9AYValladtesenaingnotricatio> D WebhookE.gitkeep© ChangeLogContextCorrelatiov @ Mailv @ Activitiesdeclarelscract-types=l)namespace Jiminny Listeners Activitles crm'›use ...c) exportLink.php24 (2class Matchcrmobnect 1mplements Shouldoueve© MailBoxFailedToConnect.p) smskeceivea.php© SmsRelavFailed.phpc) Trackkestored.ondCalendars>D crm0 Reports(c) Mailable.ohoModelsM ActivityM AskAnvthindN ConnectionM Contracts,M Crm(e) RusinessProcess.one© Configuration.php© ContactRole.php© Field.php© FieldData.phpc rielavalue.onpc Layout.onpc) Lavoutcnulv.onoc) Loq.phpC) Profile.phpC) RecordTvpe.phpc) SvncBatch.php> C7 ElasticSearch→→ Feature> MOpportunitv> M Particioant7 Plavbackithem:M Plavlist)ScorecardMWebhook(c) Account nho* Match the remote CRM relations to the particioant as theu join..public function handle(Opened|Joined $event): voicSemailHelper = app( abstract: EmailHelper::class);• TeamPenositony• •cilass)•$logger = app( abstract: LoggerInterface::class);Sparticipant = $event->getParticipantO:Sactivity = $event->getActivityO:// Don't bother if they are a team memberif (Sparticipant->isTeamMemberO) ‹slogger-sintolrauchurmuoectl rarcicipant is tean memder'onreturn"Suser = Sactivity->qetUserO:Steam = Suser->getTeamO:Sorospectsearchstrateav = Prospectsearchstrateov-actorv::motchc.team):if (SorospectSearchStrateav->ianoreCrmMatchData0) {$Logger->info('lMatchCrmObject) Ignoring crm data because of prospect strategy', l= Sactivity->cetido.'strateay' => aet class(SorosoectSearchStrateav)lneturn.Semail = Snanticinant->ae+EmailAddnecs0•i£ Semail 1== nulll&& ! SemailHelper-›shouldPerformLookup(Steam, Semail)C) Activitv nhn.$logger->info('[MatchCrm0bject] Email domain belongs to the team, skipping crm lookup'. ['activity id' => Sactivity->qetIdO.'team id' => Steam->qetIdo(C) A Promnt nhrlomaili => Comaitkal100% S2. Thu 14 May 15:16:40AskJiminnyReportActivityServiceTest v= custom.logscratch &.ison= laravel.logA SF jiminny@localhost]& console [PROD]CascadeA console (EU]tiò accounts [EU]fii stages (EU]iiò teams [EU]© Activity.php xPipedrive SDK EvaluaFixing Activity StagImport CRM Activity T+0 ..© FixActivitiesOpportunity.phg& console SlAGiNGclass Accvicy excenas model Imolemencs84 B169 M4 M 102 24 ^120782079208920912092hatenus on CActivaty. php#L2091-2163 . Wien is i being Used? Give me each iow and also when doe siwepacam bool senterroom true if participant is entering the room. false if we just want to update some participa* Idnacam cocbon nuuenterinme t entertows toue then this1s the norn tome when the actua enter has occunneoublic function uodate0rcreateParticioantaarray $data,bool SenterRoom = truel?Carbon SenterTime = null,bool SnameMatchina = falselPantricinant y..%* Updates participant CRM data* @param arrayiAccount/nucl,Opportunity|null,contace nuut.stage nullstring|null*r Srecords* doaram Participant sparticipant participant the cki data 1s associated withpublic function updateParticipantCrmData(array Srecords. Participant Sparticipant): voids...?* Uodates activitu CRi data* Aoaram acrausOnnortunitulnull.Contact/null.Stage|null,strinalnullEytract Surround/=:function undatoßctivitvComhataliannav Cnoconde)• voidiCascade I-lCommand %21// Extract the records.[Slead, Saccount, $opportunity, $contact, $stagel = $records:Gnocolvon - Cthic-sao+lindateCnmhataPocolvon/).Sstrategy = Sresolver->resolveForActivity(Slead, $contact, $account):searcned updateactiviycrmbata In appThought for 1skead ImportParticipants.onp and s other tiles >Thoucht for 3s )Searched class.*Listener in app/Listeners/Activities/CrmThought for 1s >Searched Matchcrmobiect::class in appError during tool executionDID DEND TN TMN 1 HJed weekly usage quota is exhausted. Purchase extra usage to continue using premium models. Quota resets May 17, 11:00" AN cuTи.Ask anvthina (&4L.if (Sstrategy == UpdateCrmDataByStrateav::Lead) {!' Also update the parent activity if required, checking we don't create a mixed lead/account recond.« Code SWF-1.6W Windsurf Toams 2001-5 /2024 charc 72 line hroake)UTF.8io 4 spaces...
|
NULL
|
-4368548807687433372
|
NULL
|
click
|
ocr
|
NULL
|
PhostormVIewINavicarecodeLaravelKeractorWindowFV f PhostormVIewINavicarecodeLaravelKeractorWindowFV faVsco.js( #12077 on JY-20903-update_activity-stage-on...haroledeyUserinvitationbtU.onp© ImportParticipants.php© SyncPlanhat.php© UpdateCrmData.php X© MatchCrmObject.php XO syncuserrllot.onp© ValidateSendingMessage.psiphow2V9AYValladtesenaingnotricatio> D WebhookE.gitkeep© ChangeLogContextCorrelatiov @ Mailv @ Activitiesdeclarelscract-types=l)namespace Jiminny Listeners Activitles crm'›use ...c) exportLink.php24 (2class Matchcrmobnect 1mplements Shouldoueve© MailBoxFailedToConnect.p) smskeceivea.php© SmsRelavFailed.phpc) Trackkestored.ondCalendars>D crm0 Reports(c) Mailable.ohoModelsM ActivityM AskAnvthindN ConnectionM Contracts,M Crm(e) RusinessProcess.one© Configuration.php© ContactRole.php© Field.php© FieldData.phpc rielavalue.onpc Layout.onpc) Lavoutcnulv.onoc) Loq.phpC) Profile.phpC) RecordTvpe.phpc) SvncBatch.php> C7 ElasticSearch→→ Feature> MOpportunitv> M Particioant7 Plavbackithem:M Plavlist)ScorecardMWebhook(c) Account nho* Match the remote CRM relations to the particioant as theu join..public function handle(Opened|Joined $event): voicSemailHelper = app( abstract: EmailHelper::class);• TeamPenositony• •cilass)•$logger = app( abstract: LoggerInterface::class);Sparticipant = $event->getParticipantO:Sactivity = $event->getActivityO:// Don't bother if they are a team memberif (Sparticipant->isTeamMemberO) ‹slogger-sintolrauchurmuoectl rarcicipant is tean memder'onreturn"Suser = Sactivity->qetUserO:Steam = Suser->getTeamO:Sorospectsearchstrateav = Prospectsearchstrateov-actorv::motchc.team):if (SorospectSearchStrateav->ianoreCrmMatchData0) {$Logger->info('lMatchCrmObject) Ignoring crm data because of prospect strategy', l= Sactivity->cetido.'strateay' => aet class(SorosoectSearchStrateav)lneturn.Semail = Snanticinant->ae+EmailAddnecs0•i£ Semail 1== nulll&& ! SemailHelper-›shouldPerformLookup(Steam, Semail)C) Activitv nhn.$logger->info('[MatchCrm0bject] Email domain belongs to the team, skipping crm lookup'. ['activity id' => Sactivity->qetIdO.'team id' => Steam->qetIdo(C) A Promnt nhrlomaili => Comaitkal100% S2. Thu 14 May 15:16:40AskJiminnyReportActivityServiceTest v= custom.logscratch &.ison= laravel.logA SF jiminny@localhost]& console [PROD]CascadeA console (EU]tiò accounts [EU]fii stages (EU]iiò teams [EU]© Activity.php xPipedrive SDK EvaluaFixing Activity StagImport CRM Activity T+0 ..© FixActivitiesOpportunity.phg& console SlAGiNGclass Accvicy excenas model Imolemencs84 B169 M4 M 102 24 ^120782079208920912092hatenus on CActivaty. php#L2091-2163 . Wien is i being Used? Give me each iow and also when doe siwepacam bool senterroom true if participant is entering the room. false if we just want to update some participa* Idnacam cocbon nuuenterinme t entertows toue then this1s the norn tome when the actua enter has occunneoublic function uodate0rcreateParticioantaarray $data,bool SenterRoom = truel?Carbon SenterTime = null,bool SnameMatchina = falselPantricinant y..%* Updates participant CRM data* @param arrayiAccount/nucl,Opportunity|null,contace nuut.stage nullstring|null*r Srecords* doaram Participant sparticipant participant the cki data 1s associated withpublic function updateParticipantCrmData(array Srecords. Participant Sparticipant): voids...?* Uodates activitu CRi data* Aoaram acrausOnnortunitulnull.Contact/null.Stage|null,strinalnullEytract Surround/=:function undatoßctivitvComhataliannav Cnoconde)• voidiCascade I-lCommand %21// Extract the records.[Slead, Saccount, $opportunity, $contact, $stagel = $records:Gnocolvon - Cthic-sao+lindateCnmhataPocolvon/).Sstrategy = Sresolver->resolveForActivity(Slead, $contact, $account):searcned updateactiviycrmbata In appThought for 1skead ImportParticipants.onp and s other tiles >Thoucht for 3s )Searched class.*Listener in app/Listeners/Activities/CrmThought for 1s >Searched Matchcrmobiect::class in appError during tool executionDID DEND TN TMN 1 HJed weekly usage quota is exhausted. Purchase extra usage to continue using premium models. Quota resets May 17, 11:00" AN cuTи.Ask anvthina (&4L.if (Sstrategy == UpdateCrmDataByStrateav::Lead) {!' Also update the parent activity if required, checking we don't create a mixed lead/account recond.« Code SWF-1.6W Windsurf Toams 2001-5 /2024 charc 72 line hroake)UTF.8io 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
43047
|
1572
|
79
|
2026-05-14T12:16:39.487065+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760999487_m2.jpg...
|
PhpStorm
|
faVsco.js – Activity.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\Activities\Crm;
use Illuminate\Contracts\Queue\ShouldQueue;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Events\Activities\Participants\Joined;
use Jiminny\Events\Activities\Connections\Opened;
use Jiminny\Exceptions\HttpSessionExpiredException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models;
use Jiminny\Models\Stage;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Jiminny\Services\Crm\ProspectSearchStrategyFactory;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
class MatchCrmObject implements ShouldQueue
{
/**
* Match the remote CRM relations to the participant as they join.
*/
public function handle(Opened|Joined $event): void
{
$emailHelper = app(EmailHelper::class);
$teamRepository = app(TeamRepository::class);
$logger = app(LoggerInterface::class);
$participant = $event->getParticipant();
$activity = $event->getActivity();
// Don't bother if they are a team member.
if ($participant->isTeamMember()) {
$logger->info('[MatchCrmObject] Participant is team member');
return;
}
$user = $activity->getUser();
$team = $user->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$logger->info('[MatchCrmObject] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
$email = $participant->getEmailAddress();
if ($email !== null
&& ! $emailHelper->shouldPerformLookup($team, $email)
) {
$logger->info('[MatchCrmObject] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $email,
]);
$teamMember = $teamRepository->findActiveTeamMemberByEmail($team, $email);
if ($teamMember instanceof Models\User) {
$participant->setUserId($teamMember->getId());
$participant->save();
}
return;
}
// If we have previously matched their crm identity,
// sync the crm object to get the freshest state, then skip any further lookup
if ($participant->getLeadId() !== null || $participant->getContactId() !== null) {
$this->syncActivityCrmFromParticipant($activity, $participant, $logger);
$logger->info('[MatchCrmObject] Participant already has a crm prospect identified, skipping crm lookup', [
'activity_id' => $activity->getId(),
'email' => $email,
]);
return;
}
try {
$crmService = $this->getCrmService($team, $user);
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$records = null;
if ($email !== null) {
$records = $decorator->matchExactlyByEmail($email, $user->getId());
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $decorator->matchByPhone($participant->getPhoneNumber(), null, $user->getId());
}
if (empty($records) && $email !== null) {
$records = $decorator->matchByDomain($email, $user->getId());
}
if (! empty($records)) {
$activity->updateParticipantCrmData($records, $participant);
$activity->updateActivityCrmData($records);
}
} catch (HttpSessionExpiredException|SocialAccountTokenInvalidException $exception) {
$logger->info('[MatchCrmObject] Activity user triggered exception', [
'user_id' => $activity->getUserId(),
'activity_id' => $activity->getId(),
'exception_message' => $exception->getMessage(),
'exception_code' => $exception->getCode(),
]);
}
}
/**
* @throws SocialAccountTokenInvalidException
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
private function getCrmService(Models\Team $team, Models\User $user): ServiceInterface
{
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
return $crmResolver->prepareCrmService();
}
/**
* Sync CRM identifiers from a matched participant onto the activity when appropriate.
*/
private function syncActivityCrmFromParticipant(
Models\Activity $activity,
Models\Participant $participant,
LoggerInterface $logger
): void {
$hasUpdate = false;
if ($participant->getContactId() !== null && $activity->getContactId() === null) {
$logger->info('[MatchCrmObject] Syncing contact_id from participant to activity', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'from_contact_id' => $activity->getContactId(),
'to_contact_id' => $participant->getContactId(),
]);
$activity->setLeadId(null);
if ($activity->getStage() !== null && $activity->getStage()->getType() === Stage::TYPE_LEAD) {
$logger->info('[MatchCrmObject] Clearing lead stage on activity due to contact match', [
'activity_id' => $activity->getId(),
'stage_id' => $activity->getStageId(),
]);
$activity->setStageId(null);
}
$activity->setContactId($participant->getContactId());
$logger->info('[MatchCrmObject] Activity contact_id set', [
'activity_id' => $activity->getId(),
'contact_id' => $activity->getContactId(),
]);
$hasUpdate = true;
}
if ($participant->getLeadId() !== null
&& $activity->getLeadId() === null
&& $activity->getContactId() === null
&& $activity->getAccountId() === null
) {
$logger->info('[MatchCrmObject] Syncing lead_id from participant to activity', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'lead_id' => $participant->getLeadId(),
]);
$activity->setLeadId($participant->getLeadId());
$hasUpdate = true;
}
if ($hasUpdate) {
$activity->save();
}
}
}
Sync Changes
Hide This Notification...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.12566489,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.37400267,"top":0.10055866,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"9","depth":4,"bounds":{"left":0.38397607,"top":0.10055866,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.39361703,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.40093085,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\Activities\\Crm;\n\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Events\\Activities\\Participants\\Joined;\nuse Jiminny\\Events\\Activities\\Connections\\Opened;\nuse Jiminny\\Exceptions\\HttpSessionExpiredException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Jiminny\\Services\\Crm\\ProspectSearchStrategyFactory;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\n\nclass MatchCrmObject implements ShouldQueue\n{\n /**\n * Match the remote CRM relations to the participant as they join.\n */\n public function handle(Opened|Joined $event): void\n {\n $emailHelper = app(EmailHelper::class);\n $teamRepository = app(TeamRepository::class);\n $logger = app(LoggerInterface::class);\n\n $participant = $event->getParticipant();\n $activity = $event->getActivity();\n\n // Don't bother if they are a team member.\n if ($participant->isTeamMember()) {\n $logger->info('[MatchCrmObject] Participant is team member');\n\n return;\n }\n\n $user = $activity->getUser();\n $team = $user->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $logger->info('[MatchCrmObject] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n $email = $participant->getEmailAddress();\n if ($email !== null\n && ! $emailHelper->shouldPerformLookup($team, $email)\n ) {\n $logger->info('[MatchCrmObject] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $email,\n ]);\n\n $teamMember = $teamRepository->findActiveTeamMemberByEmail($team, $email);\n if ($teamMember instanceof Models\\User) {\n $participant->setUserId($teamMember->getId());\n $participant->save();\n }\n\n return;\n }\n\n // If we have previously matched their crm identity,\n // sync the crm object to get the freshest state, then skip any further lookup\n if ($participant->getLeadId() !== null || $participant->getContactId() !== null) {\n $this->syncActivityCrmFromParticipant($activity, $participant, $logger);\n\n $logger->info('[MatchCrmObject] Participant already has a crm prospect identified, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'email' => $email,\n ]);\n\n return;\n }\n\n try {\n $crmService = $this->getCrmService($team, $user);\n\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $records = null;\n\n if ($email !== null) {\n $records = $decorator->matchExactlyByEmail($email, $user->getId());\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $decorator->matchByPhone($participant->getPhoneNumber(), null, $user->getId());\n }\n\n if (empty($records) && $email !== null) {\n $records = $decorator->matchByDomain($email, $user->getId());\n }\n\n if (! empty($records)) {\n $activity->updateParticipantCrmData($records, $participant);\n $activity->updateActivityCrmData($records);\n }\n } catch (HttpSessionExpiredException|SocialAccountTokenInvalidException $exception) {\n $logger->info('[MatchCrmObject] Activity user triggered exception', [\n 'user_id' => $activity->getUserId(),\n 'activity_id' => $activity->getId(),\n 'exception_message' => $exception->getMessage(),\n 'exception_code' => $exception->getCode(),\n ]);\n }\n }\n\n /**\n * @throws SocialAccountTokenInvalidException\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n */\n private function getCrmService(Models\\Team $team, Models\\User $user): ServiceInterface\n {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n\n return $crmResolver->prepareCrmService();\n }\n\n /**\n * Sync CRM identifiers from a matched participant onto the activity when appropriate.\n */\n private function syncActivityCrmFromParticipant(\n Models\\Activity $activity,\n Models\\Participant $participant,\n LoggerInterface $logger\n ): void {\n $hasUpdate = false;\n\n if ($participant->getContactId() !== null && $activity->getContactId() === null) {\n $logger->info('[MatchCrmObject] Syncing contact_id from participant to activity', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'from_contact_id' => $activity->getContactId(),\n 'to_contact_id' => $participant->getContactId(),\n ]);\n\n $activity->setLeadId(null);\n\n if ($activity->getStage() !== null && $activity->getStage()->getType() === Stage::TYPE_LEAD) {\n $logger->info('[MatchCrmObject] Clearing lead stage on activity due to contact match', [\n 'activity_id' => $activity->getId(),\n 'stage_id' => $activity->getStageId(),\n ]);\n $activity->setStageId(null);\n }\n\n $activity->setContactId($participant->getContactId());\n\n $logger->info('[MatchCrmObject] Activity contact_id set', [\n 'activity_id' => $activity->getId(),\n 'contact_id' => $activity->getContactId(),\n ]);\n\n $hasUpdate = true;\n }\n\n if ($participant->getLeadId() !== null\n && $activity->getLeadId() === null\n && $activity->getContactId() === null\n && $activity->getAccountId() === null\n ) {\n $logger->info('[MatchCrmObject] Syncing lead_id from participant to activity', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'lead_id' => $participant->getLeadId(),\n ]);\n\n $activity->setLeadId($participant->getLeadId());\n $hasUpdate = true;\n }\n\n if ($hasUpdate) {\n $activity->save();\n }\n }\n}","depth":4,"bounds":{"left":0.122340426,"top":0.09736632,"width":0.32214096,"height":0.90263367},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\Activities\\Crm;\n\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Events\\Activities\\Participants\\Joined;\nuse Jiminny\\Events\\Activities\\Connections\\Opened;\nuse Jiminny\\Exceptions\\HttpSessionExpiredException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Jiminny\\Services\\Crm\\ProspectSearchStrategyFactory;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\n\nclass MatchCrmObject implements ShouldQueue\n{\n /**\n * Match the remote CRM relations to the participant as they join.\n */\n public function handle(Opened|Joined $event): void\n {\n $emailHelper = app(EmailHelper::class);\n $teamRepository = app(TeamRepository::class);\n $logger = app(LoggerInterface::class);\n\n $participant = $event->getParticipant();\n $activity = $event->getActivity();\n\n // Don't bother if they are a team member.\n if ($participant->isTeamMember()) {\n $logger->info('[MatchCrmObject] Participant is team member');\n\n return;\n }\n\n $user = $activity->getUser();\n $team = $user->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $logger->info('[MatchCrmObject] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n $email = $participant->getEmailAddress();\n if ($email !== null\n && ! $emailHelper->shouldPerformLookup($team, $email)\n ) {\n $logger->info('[MatchCrmObject] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $email,\n ]);\n\n $teamMember = $teamRepository->findActiveTeamMemberByEmail($team, $email);\n if ($teamMember instanceof Models\\User) {\n $participant->setUserId($teamMember->getId());\n $participant->save();\n }\n\n return;\n }\n\n // If we have previously matched their crm identity,\n // sync the crm object to get the freshest state, then skip any further lookup\n if ($participant->getLeadId() !== null || $participant->getContactId() !== null) {\n $this->syncActivityCrmFromParticipant($activity, $participant, $logger);\n\n $logger->info('[MatchCrmObject] Participant already has a crm prospect identified, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'email' => $email,\n ]);\n\n return;\n }\n\n try {\n $crmService = $this->getCrmService($team, $user);\n\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $records = null;\n\n if ($email !== null) {\n $records = $decorator->matchExactlyByEmail($email, $user->getId());\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $decorator->matchByPhone($participant->getPhoneNumber(), null, $user->getId());\n }\n\n if (empty($records) && $email !== null) {\n $records = $decorator->matchByDomain($email, $user->getId());\n }\n\n if (! empty($records)) {\n $activity->updateParticipantCrmData($records, $participant);\n $activity->updateActivityCrmData($records);\n }\n } catch (HttpSessionExpiredException|SocialAccountTokenInvalidException $exception) {\n $logger->info('[MatchCrmObject] Activity user triggered exception', [\n 'user_id' => $activity->getUserId(),\n 'activity_id' => $activity->getId(),\n 'exception_message' => $exception->getMessage(),\n 'exception_code' => $exception->getCode(),\n ]);\n }\n }\n\n /**\n * @throws SocialAccountTokenInvalidException\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n */\n private function getCrmService(Models\\Team $team, Models\\User $user): ServiceInterface\n {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n\n return $crmResolver->prepareCrmService();\n }\n\n /**\n * Sync CRM identifiers from a matched participant onto the activity when appropriate.\n */\n private function syncActivityCrmFromParticipant(\n Models\\Activity $activity,\n Models\\Participant $participant,\n LoggerInterface $logger\n ): void {\n $hasUpdate = false;\n\n if ($participant->getContactId() !== null && $activity->getContactId() === null) {\n $logger->info('[MatchCrmObject] Syncing contact_id from participant to activity', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'from_contact_id' => $activity->getContactId(),\n 'to_contact_id' => $participant->getContactId(),\n ]);\n\n $activity->setLeadId(null);\n\n if ($activity->getStage() !== null && $activity->getStage()->getType() === Stage::TYPE_LEAD) {\n $logger->info('[MatchCrmObject] Clearing lead stage on activity due to contact match', [\n 'activity_id' => $activity->getId(),\n 'stage_id' => $activity->getStageId(),\n ]);\n $activity->setStageId(null);\n }\n\n $activity->setContactId($participant->getContactId());\n\n $logger->info('[MatchCrmObject] Activity contact_id set', [\n 'activity_id' => $activity->getId(),\n 'contact_id' => $activity->getContactId(),\n ]);\n\n $hasUpdate = true;\n }\n\n if ($participant->getLeadId() !== null\n && $activity->getLeadId() === null\n && $activity->getContactId() === null\n && $activity->getAccountId() === null\n ) {\n $logger->info('[MatchCrmObject] Syncing lead_id from participant to activity', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'lead_id' => $participant->getLeadId(),\n ]);\n\n $activity->setLeadId($participant->getLeadId());\n $hasUpdate = true;\n }\n\n if ($hasUpdate) {\n $activity->save();\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-3568872732757251829
|
-3896512396629267706
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\Activities\Crm;
use Illuminate\Contracts\Queue\ShouldQueue;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Events\Activities\Participants\Joined;
use Jiminny\Events\Activities\Connections\Opened;
use Jiminny\Exceptions\HttpSessionExpiredException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models;
use Jiminny\Models\Stage;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Jiminny\Services\Crm\ProspectSearchStrategyFactory;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
class MatchCrmObject implements ShouldQueue
{
/**
* Match the remote CRM relations to the participant as they join.
*/
public function handle(Opened|Joined $event): void
{
$emailHelper = app(EmailHelper::class);
$teamRepository = app(TeamRepository::class);
$logger = app(LoggerInterface::class);
$participant = $event->getParticipant();
$activity = $event->getActivity();
// Don't bother if they are a team member.
if ($participant->isTeamMember()) {
$logger->info('[MatchCrmObject] Participant is team member');
return;
}
$user = $activity->getUser();
$team = $user->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$logger->info('[MatchCrmObject] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
$email = $participant->getEmailAddress();
if ($email !== null
&& ! $emailHelper->shouldPerformLookup($team, $email)
) {
$logger->info('[MatchCrmObject] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $email,
]);
$teamMember = $teamRepository->findActiveTeamMemberByEmail($team, $email);
if ($teamMember instanceof Models\User) {
$participant->setUserId($teamMember->getId());
$participant->save();
}
return;
}
// If we have previously matched their crm identity,
// sync the crm object to get the freshest state, then skip any further lookup
if ($participant->getLeadId() !== null || $participant->getContactId() !== null) {
$this->syncActivityCrmFromParticipant($activity, $participant, $logger);
$logger->info('[MatchCrmObject] Participant already has a crm prospect identified, skipping crm lookup', [
'activity_id' => $activity->getId(),
'email' => $email,
]);
return;
}
try {
$crmService = $this->getCrmService($team, $user);
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$records = null;
if ($email !== null) {
$records = $decorator->matchExactlyByEmail($email, $user->getId());
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $decorator->matchByPhone($participant->getPhoneNumber(), null, $user->getId());
}
if (empty($records) && $email !== null) {
$records = $decorator->matchByDomain($email, $user->getId());
}
if (! empty($records)) {
$activity->updateParticipantCrmData($records, $participant);
$activity->updateActivityCrmData($records);
}
} catch (HttpSessionExpiredException|SocialAccountTokenInvalidException $exception) {
$logger->info('[MatchCrmObject] Activity user triggered exception', [
'user_id' => $activity->getUserId(),
'activity_id' => $activity->getId(),
'exception_message' => $exception->getMessage(),
'exception_code' => $exception->getCode(),
]);
}
}
/**
* @throws SocialAccountTokenInvalidException
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
private function getCrmService(Models\Team $team, Models\User $user): ServiceInterface
{
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
return $crmResolver->prepareCrmService();
}
/**
* Sync CRM identifiers from a matched participant onto the activity when appropriate.
*/
private function syncActivityCrmFromParticipant(
Models\Activity $activity,
Models\Participant $participant,
LoggerInterface $logger
): void {
$hasUpdate = false;
if ($participant->getContactId() !== null && $activity->getContactId() === null) {
$logger->info('[MatchCrmObject] Syncing contact_id from participant to activity', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'from_contact_id' => $activity->getContactId(),
'to_contact_id' => $participant->getContactId(),
]);
$activity->setLeadId(null);
if ($activity->getStage() !== null && $activity->getStage()->getType() === Stage::TYPE_LEAD) {
$logger->info('[MatchCrmObject] Clearing lead stage on activity due to contact match', [
'activity_id' => $activity->getId(),
'stage_id' => $activity->getStageId(),
]);
$activity->setStageId(null);
}
$activity->setContactId($participant->getContactId());
$logger->info('[MatchCrmObject] Activity contact_id set', [
'activity_id' => $activity->getId(),
'contact_id' => $activity->getContactId(),
]);
$hasUpdate = true;
}
if ($participant->getLeadId() !== null
&& $activity->getLeadId() === null
&& $activity->getContactId() === null
&& $activity->getAccountId() === null
) {
$logger->info('[MatchCrmObject] Syncing lead_id from participant to activity', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'lead_id' => $participant->getLeadId(),
]);
$activity->setLeadId($participant->getLeadId());
$hasUpdate = true;
}
if ($hasUpdate) {
$activity->save();
}
}
}
Sync Changes
Hide This Notification...
|
43045
|
NULL
|
NULL
|
NULL
|
|
43046
|
1571
|
54
|
2026-05-14T12:16:12.744345+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760972744_m1.jpg...
|
PhpStorm
|
faVsco.js – Activity.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\Activities\Crm;
use Illuminate\Contracts\Queue\ShouldQueue;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Events\Activities\Participants\Joined;
use Jiminny\Events\Activities\Connections\Opened;
use Jiminny\Exceptions\HttpSessionExpiredException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models;
use Jiminny\Models\Stage;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Jiminny\Services\Crm\ProspectSearchStrategyFactory;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
class MatchCrmObject implements ShouldQueue
{
/**
* Match the remote CRM relations to the participant as they join.
*/
public function handle(Opened|Joined $event): void
{
$emailHelper = app(EmailHelper::class);
$teamRepository = app(TeamRepository::class);
$logger = app(LoggerInterface::class);
$participant = $event->getParticipant();
$activity = $event->getActivity();
// Don't bother if they are a team member.
if ($participant->isTeamMember()) {
$logger->info('[MatchCrmObject] Participant is team member');
return;
}
$user = $activity->getUser();
$team = $user->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$logger->info('[MatchCrmObject] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
$email = $participant->getEmailAddress();
if ($email !== null
&& ! $emailHelper->shouldPerformLookup($team, $email)
) {
$logger->info('[MatchCrmObject] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $email,
]);
$teamMember = $teamRepository->findActiveTeamMemberByEmail($team, $email);
if ($teamMember instanceof Models\User) {
$participant->setUserId($teamMember->getId());
$participant->save();
}
return;
}
// If we have previously matched their crm identity,
// sync the crm object to get the freshest state, then skip any further lookup
if ($participant->getLeadId() !== null || $participant->getContactId() !== null) {
$this->syncActivityCrmFromParticipant($activity, $participant, $logger);
$logger->info('[MatchCrmObject] Participant already has a crm prospect identified, skipping crm lookup', [
'activity_id' => $activity->getId(),
'email' => $email,
]);
return;
}
try {
$crmService = $this->getCrmService($team, $user);
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$records = null;
if ($email !== null) {
$records = $decorator->matchExactlyByEmail($email, $user->getId());
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $decorator->matchByPhone($participant->getPhoneNumber(), null, $user->getId());
}
if (empty($records) && $email !== null) {
$records = $decorator->matchByDomain($email, $user->getId());
}
if (! empty($records)) {
$activity->updateParticipantCrmData($records, $participant);
$activity->updateActivityCrmData($records);
}
} catch (HttpSessionExpiredException|SocialAccountTokenInvalidException $exception) {
$logger->info('[MatchCrmObject] Activity user triggered exception', [
'user_id' => $activity->getUserId(),
'activity_id' => $activity->getId(),
'exception_message' => $exception->getMessage(),
'exception_code' => $exception->getCode(),
]);
}
}
/**
* @throws SocialAccountTokenInvalidException
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
private function getCrmService(Models\Team $team, Models\User $user): ServiceInterface
{
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
return $crmResolver->prepareCrmService();
}
/**
* Sync CRM identifiers from a matched participant onto the activity when appropriate.
*/
private function syncActivityCrmFromParticipant(
Models\Activity $activity,
Models\Participant $participant,
LoggerInterface $logger
): void {
$hasUpdate = false;
if ($participant->getContactId() !== null && $activity->getContactId() === null) {
$logger->info('[MatchCrmObject] Syncing contact_id from participant to activity', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'from_contact_id' => $activity->getContactId(),
'to_contact_id' => $participant->getContactId(),
]);
$activity->setLeadId(null);
if ($activity->getStage() !== null && $activity->getStage()->getType() === Stage::TYPE_LEAD) {
$logger->info('[MatchCrmObject] Clearing lead stage on activity due to contact match', [
'activity_id' => $activity->getId(),
'stage_id' => $activity->getStageId(),
]);
$activity->setStageId(null);
}
$activity->setContactId($participant->getContactId());
$logger->info('[MatchCrmObject] Activity contact_id set', [
'activity_id' => $activity->getId(),
'contact_id' => $activity->getContactId(),
]);
$hasUpdate = true;
}
if ($participant->getLeadId() !== null
&& $activity->getLeadId() === null
&& $activity->getContactId() === null
&& $activity->getAccountId() === null
) {
$logger->info('[MatchCrmObject] Syncing lead_id from participant to activity', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'lead_id' => $participant->getLeadId(),
]);
$activity->setLeadId($participant->getLeadId());
$hasUpdate = true;
}
if ($hasUpdate) {
$activity->save();
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preference' => 'boolean',
'recording_reason_code' => 'integer',
'has_recording_prompt' => 'boolean',
'on_air' => 'integer',
];
}
protected static function boot()
{
parent::boot();
static::updated(static function (Activity $activity) {
// If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week
if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||
($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {
if ($activity->isDirty('status')) {
event(new StatusUpdated($activity));
}
if ($activity->isDirty('stage_id')) {
event(new StageUpdated($activity));
}
if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {
event(new ProspectUpdated($activity));
}
if ($activity->isDirty('opportunity_id')) {
event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));
}
if ($activity->isDirty('title')) {
event(new TitleUpdated($activity));
}
}
if ($activity->isDirty('playbook_category_id')) {
event(new ActivityTypeUpdated($activity));
}
});
static:...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"9","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\Activities\\Crm;\n\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Events\\Activities\\Participants\\Joined;\nuse Jiminny\\Events\\Activities\\Connections\\Opened;\nuse Jiminny\\Exceptions\\HttpSessionExpiredException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Jiminny\\Services\\Crm\\ProspectSearchStrategyFactory;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\n\nclass MatchCrmObject implements ShouldQueue\n{\n /**\n * Match the remote CRM relations to the participant as they join.\n */\n public function handle(Opened|Joined $event): void\n {\n $emailHelper = app(EmailHelper::class);\n $teamRepository = app(TeamRepository::class);\n $logger = app(LoggerInterface::class);\n\n $participant = $event->getParticipant();\n $activity = $event->getActivity();\n\n // Don't bother if they are a team member.\n if ($participant->isTeamMember()) {\n $logger->info('[MatchCrmObject] Participant is team member');\n\n return;\n }\n\n $user = $activity->getUser();\n $team = $user->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $logger->info('[MatchCrmObject] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n $email = $participant->getEmailAddress();\n if ($email !== null\n && ! $emailHelper->shouldPerformLookup($team, $email)\n ) {\n $logger->info('[MatchCrmObject] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $email,\n ]);\n\n $teamMember = $teamRepository->findActiveTeamMemberByEmail($team, $email);\n if ($teamMember instanceof Models\\User) {\n $participant->setUserId($teamMember->getId());\n $participant->save();\n }\n\n return;\n }\n\n // If we have previously matched their crm identity,\n // sync the crm object to get the freshest state, then skip any further lookup\n if ($participant->getLeadId() !== null || $participant->getContactId() !== null) {\n $this->syncActivityCrmFromParticipant($activity, $participant, $logger);\n\n $logger->info('[MatchCrmObject] Participant already has a crm prospect identified, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'email' => $email,\n ]);\n\n return;\n }\n\n try {\n $crmService = $this->getCrmService($team, $user);\n\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $records = null;\n\n if ($email !== null) {\n $records = $decorator->matchExactlyByEmail($email, $user->getId());\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $decorator->matchByPhone($participant->getPhoneNumber(), null, $user->getId());\n }\n\n if (empty($records) && $email !== null) {\n $records = $decorator->matchByDomain($email, $user->getId());\n }\n\n if (! empty($records)) {\n $activity->updateParticipantCrmData($records, $participant);\n $activity->updateActivityCrmData($records);\n }\n } catch (HttpSessionExpiredException|SocialAccountTokenInvalidException $exception) {\n $logger->info('[MatchCrmObject] Activity user triggered exception', [\n 'user_id' => $activity->getUserId(),\n 'activity_id' => $activity->getId(),\n 'exception_message' => $exception->getMessage(),\n 'exception_code' => $exception->getCode(),\n ]);\n }\n }\n\n /**\n * @throws SocialAccountTokenInvalidException\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n */\n private function getCrmService(Models\\Team $team, Models\\User $user): ServiceInterface\n {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n\n return $crmResolver->prepareCrmService();\n }\n\n /**\n * Sync CRM identifiers from a matched participant onto the activity when appropriate.\n */\n private function syncActivityCrmFromParticipant(\n Models\\Activity $activity,\n Models\\Participant $participant,\n LoggerInterface $logger\n ): void {\n $hasUpdate = false;\n\n if ($participant->getContactId() !== null && $activity->getContactId() === null) {\n $logger->info('[MatchCrmObject] Syncing contact_id from participant to activity', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'from_contact_id' => $activity->getContactId(),\n 'to_contact_id' => $participant->getContactId(),\n ]);\n\n $activity->setLeadId(null);\n\n if ($activity->getStage() !== null && $activity->getStage()->getType() === Stage::TYPE_LEAD) {\n $logger->info('[MatchCrmObject] Clearing lead stage on activity due to contact match', [\n 'activity_id' => $activity->getId(),\n 'stage_id' => $activity->getStageId(),\n ]);\n $activity->setStageId(null);\n }\n\n $activity->setContactId($participant->getContactId());\n\n $logger->info('[MatchCrmObject] Activity contact_id set', [\n 'activity_id' => $activity->getId(),\n 'contact_id' => $activity->getContactId(),\n ]);\n\n $hasUpdate = true;\n }\n\n if ($participant->getLeadId() !== null\n && $activity->getLeadId() === null\n && $activity->getContactId() === null\n && $activity->getAccountId() === null\n ) {\n $logger->info('[MatchCrmObject] Syncing lead_id from participant to activity', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'lead_id' => $participant->getLeadId(),\n ]);\n\n $activity->setLeadId($participant->getLeadId());\n $hasUpdate = true;\n }\n\n if ($hasUpdate) {\n $activity->save();\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\Activities\\Crm;\n\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Events\\Activities\\Participants\\Joined;\nuse Jiminny\\Events\\Activities\\Connections\\Opened;\nuse Jiminny\\Exceptions\\HttpSessionExpiredException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Jiminny\\Services\\Crm\\ProspectSearchStrategyFactory;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\n\nclass MatchCrmObject implements ShouldQueue\n{\n /**\n * Match the remote CRM relations to the participant as they join.\n */\n public function handle(Opened|Joined $event): void\n {\n $emailHelper = app(EmailHelper::class);\n $teamRepository = app(TeamRepository::class);\n $logger = app(LoggerInterface::class);\n\n $participant = $event->getParticipant();\n $activity = $event->getActivity();\n\n // Don't bother if they are a team member.\n if ($participant->isTeamMember()) {\n $logger->info('[MatchCrmObject] Participant is team member');\n\n return;\n }\n\n $user = $activity->getUser();\n $team = $user->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $logger->info('[MatchCrmObject] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n $email = $participant->getEmailAddress();\n if ($email !== null\n && ! $emailHelper->shouldPerformLookup($team, $email)\n ) {\n $logger->info('[MatchCrmObject] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $email,\n ]);\n\n $teamMember = $teamRepository->findActiveTeamMemberByEmail($team, $email);\n if ($teamMember instanceof Models\\User) {\n $participant->setUserId($teamMember->getId());\n $participant->save();\n }\n\n return;\n }\n\n // If we have previously matched their crm identity,\n // sync the crm object to get the freshest state, then skip any further lookup\n if ($participant->getLeadId() !== null || $participant->getContactId() !== null) {\n $this->syncActivityCrmFromParticipant($activity, $participant, $logger);\n\n $logger->info('[MatchCrmObject] Participant already has a crm prospect identified, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'email' => $email,\n ]);\n\n return;\n }\n\n try {\n $crmService = $this->getCrmService($team, $user);\n\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $records = null;\n\n if ($email !== null) {\n $records = $decorator->matchExactlyByEmail($email, $user->getId());\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $decorator->matchByPhone($participant->getPhoneNumber(), null, $user->getId());\n }\n\n if (empty($records) && $email !== null) {\n $records = $decorator->matchByDomain($email, $user->getId());\n }\n\n if (! empty($records)) {\n $activity->updateParticipantCrmData($records, $participant);\n $activity->updateActivityCrmData($records);\n }\n } catch (HttpSessionExpiredException|SocialAccountTokenInvalidException $exception) {\n $logger->info('[MatchCrmObject] Activity user triggered exception', [\n 'user_id' => $activity->getUserId(),\n 'activity_id' => $activity->getId(),\n 'exception_message' => $exception->getMessage(),\n 'exception_code' => $exception->getCode(),\n ]);\n }\n }\n\n /**\n * @throws SocialAccountTokenInvalidException\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n */\n private function getCrmService(Models\\Team $team, Models\\User $user): ServiceInterface\n {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n\n return $crmResolver->prepareCrmService();\n }\n\n /**\n * Sync CRM identifiers from a matched participant onto the activity when appropriate.\n */\n private function syncActivityCrmFromParticipant(\n Models\\Activity $activity,\n Models\\Participant $participant,\n LoggerInterface $logger\n ): void {\n $hasUpdate = false;\n\n if ($participant->getContactId() !== null && $activity->getContactId() === null) {\n $logger->info('[MatchCrmObject] Syncing contact_id from participant to activity', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'from_contact_id' => $activity->getContactId(),\n 'to_contact_id' => $participant->getContactId(),\n ]);\n\n $activity->setLeadId(null);\n\n if ($activity->getStage() !== null && $activity->getStage()->getType() === Stage::TYPE_LEAD) {\n $logger->info('[MatchCrmObject] Clearing lead stage on activity due to contact match', [\n 'activity_id' => $activity->getId(),\n 'stage_id' => $activity->getStageId(),\n ]);\n $activity->setStageId(null);\n }\n\n $activity->setContactId($participant->getContactId());\n\n $logger->info('[MatchCrmObject] Activity contact_id set', [\n 'activity_id' => $activity->getId(),\n 'contact_id' => $activity->getContactId(),\n ]);\n\n $hasUpdate = true;\n }\n\n if ($participant->getLeadId() !== null\n && $activity->getLeadId() === null\n && $activity->getContactId() === null\n && $activity->getAccountId() === null\n ) {\n $logger->info('[MatchCrmObject] Syncing lead_id from participant to activity', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'lead_id' => $participant->getLeadId(),\n ]);\n\n $activity->setLeadId($participant->getLeadId());\n $hasUpdate = true;\n }\n\n if ($hasUpdate) {\n $activity->save();\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"169","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"102","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
7943249111508506828
|
7035618767472895804
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\Activities\Crm;
use Illuminate\Contracts\Queue\ShouldQueue;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Events\Activities\Participants\Joined;
use Jiminny\Events\Activities\Connections\Opened;
use Jiminny\Exceptions\HttpSessionExpiredException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models;
use Jiminny\Models\Stage;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Jiminny\Services\Crm\ProspectSearchStrategyFactory;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
class MatchCrmObject implements ShouldQueue
{
/**
* Match the remote CRM relations to the participant as they join.
*/
public function handle(Opened|Joined $event): void
{
$emailHelper = app(EmailHelper::class);
$teamRepository = app(TeamRepository::class);
$logger = app(LoggerInterface::class);
$participant = $event->getParticipant();
$activity = $event->getActivity();
// Don't bother if they are a team member.
if ($participant->isTeamMember()) {
$logger->info('[MatchCrmObject] Participant is team member');
return;
}
$user = $activity->getUser();
$team = $user->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$logger->info('[MatchCrmObject] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
$email = $participant->getEmailAddress();
if ($email !== null
&& ! $emailHelper->shouldPerformLookup($team, $email)
) {
$logger->info('[MatchCrmObject] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $email,
]);
$teamMember = $teamRepository->findActiveTeamMemberByEmail($team, $email);
if ($teamMember instanceof Models\User) {
$participant->setUserId($teamMember->getId());
$participant->save();
}
return;
}
// If we have previously matched their crm identity,
// sync the crm object to get the freshest state, then skip any further lookup
if ($participant->getLeadId() !== null || $participant->getContactId() !== null) {
$this->syncActivityCrmFromParticipant($activity, $participant, $logger);
$logger->info('[MatchCrmObject] Participant already has a crm prospect identified, skipping crm lookup', [
'activity_id' => $activity->getId(),
'email' => $email,
]);
return;
}
try {
$crmService = $this->getCrmService($team, $user);
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$records = null;
if ($email !== null) {
$records = $decorator->matchExactlyByEmail($email, $user->getId());
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $decorator->matchByPhone($participant->getPhoneNumber(), null, $user->getId());
}
if (empty($records) && $email !== null) {
$records = $decorator->matchByDomain($email, $user->getId());
}
if (! empty($records)) {
$activity->updateParticipantCrmData($records, $participant);
$activity->updateActivityCrmData($records);
}
} catch (HttpSessionExpiredException|SocialAccountTokenInvalidException $exception) {
$logger->info('[MatchCrmObject] Activity user triggered exception', [
'user_id' => $activity->getUserId(),
'activity_id' => $activity->getId(),
'exception_message' => $exception->getMessage(),
'exception_code' => $exception->getCode(),
]);
}
}
/**
* @throws SocialAccountTokenInvalidException
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
private function getCrmService(Models\Team $team, Models\User $user): ServiceInterface
{
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
return $crmResolver->prepareCrmService();
}
/**
* Sync CRM identifiers from a matched participant onto the activity when appropriate.
*/
private function syncActivityCrmFromParticipant(
Models\Activity $activity,
Models\Participant $participant,
LoggerInterface $logger
): void {
$hasUpdate = false;
if ($participant->getContactId() !== null && $activity->getContactId() === null) {
$logger->info('[MatchCrmObject] Syncing contact_id from participant to activity', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'from_contact_id' => $activity->getContactId(),
'to_contact_id' => $participant->getContactId(),
]);
$activity->setLeadId(null);
if ($activity->getStage() !== null && $activity->getStage()->getType() === Stage::TYPE_LEAD) {
$logger->info('[MatchCrmObject] Clearing lead stage on activity due to contact match', [
'activity_id' => $activity->getId(),
'stage_id' => $activity->getStageId(),
]);
$activity->setStageId(null);
}
$activity->setContactId($participant->getContactId());
$logger->info('[MatchCrmObject] Activity contact_id set', [
'activity_id' => $activity->getId(),
'contact_id' => $activity->getContactId(),
]);
$hasUpdate = true;
}
if ($participant->getLeadId() !== null
&& $activity->getLeadId() === null
&& $activity->getContactId() === null
&& $activity->getAccountId() === null
) {
$logger->info('[MatchCrmObject] Syncing lead_id from participant to activity', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'lead_id' => $participant->getLeadId(),
]);
$activity->setLeadId($participant->getLeadId());
$hasUpdate = true;
}
if ($hasUpdate) {
$activity->save();
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preference' => 'boolean',
'recording_reason_code' => 'integer',
'has_recording_prompt' => 'boolean',
'on_air' => 'integer',
];
}
protected static function boot()
{
parent::boot();
static::updated(static function (Activity $activity) {
// If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week
if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||
($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {
if ($activity->isDirty('status')) {
event(new StatusUpdated($activity));
}
if ($activity->isDirty('stage_id')) {
event(new StageUpdated($activity));
}
if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {
event(new ProspectUpdated($activity));
}
if ($activity->isDirty('opportunity_id')) {
event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));
}
if ($activity->isDirty('title')) {
event(new TitleUpdated($activity));
}
}
if ($activity->isDirty('playbook_category_id')) {
event(new ActivityTypeUpdated($activity));
}
});
static:...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
43045
|
1572
|
78
|
2026-05-14T12:15:42.335551+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760942335_m2.jpg...
|
PhpStorm
|
faVsco.js – Activity.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\Activities\Crm;
use Illuminate\Contracts\Queue\ShouldQueue;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Events\Activities\Participants\Joined;
use Jiminny\Events\Activities\Connections\Opened;
use Jiminny\Exceptions\HttpSessionExpiredException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models;
use Jiminny\Models\Stage;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Jiminny\Services\Crm\ProspectSearchStrategyFactory;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
class MatchCrmObject implements ShouldQueue
{
/**
* Match the remote CRM relations to the participant as they join.
*/
public function handle(Opened|Joined $event): void
{
$emailHelper = app(EmailHelper::class);
$teamRepository = app(TeamRepository::class);
$logger = app(LoggerInterface::class);
$participant = $event->getParticipant();
$activity = $event->getActivity();
// Don't bother if they are a team member.
if ($participant->isTeamMember()) {
$logger->info('[MatchCrmObject] Participant is team member');
return;
}
$user = $activity->getUser();
$team = $user->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$logger->info('[MatchCrmObject] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
$email = $participant->getEmailAddress();
if ($email !== null
&& ! $emailHelper->shouldPerformLookup($team, $email)
) {
$logger->info('[MatchCrmObject] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $email,
]);
$teamMember = $teamRepository->findActiveTeamMemberByEmail($team, $email);
if ($teamMember instanceof Models\User) {
$participant->setUserId($teamMember->getId());
$participant->save();
}
return;
}
// If we have previously matched their crm identity,
// sync the crm object to get the freshest state, then skip any further lookup
if ($participant->getLeadId() !== null || $participant->getContactId() !== null) {
$this->syncActivityCrmFromParticipant($activity, $participant, $logger);
$logger->info('[MatchCrmObject] Participant already has a crm prospect identified, skipping crm lookup', [
'activity_id' => $activity->getId(),
'email' => $email,
]);
return;
}
try {
$crmService = $this->getCrmService($team, $user);
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$records = null;
if ($email !== null) {
$records = $decorator->matchExactlyByEmail($email, $user->getId());
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $decorator->matchByPhone($participant->getPhoneNumber(), null, $user->getId());
}
if (empty($records) && $email !== null) {
$records = $decorator->matchByDomain($email, $user->getId());
}
if (! empty($records)) {
$activity->updateParticipantCrmData($records, $participant);
$activity->updateActivityCrmData($records);
}
} catch (HttpSessionExpiredException|SocialAccountTokenInvalidException $exception) {
$logger->info('[MatchCrmObject] Activity user triggered exception', [
'user_id' => $activity->getUserId(),
'activity_id' => $activity->getId(),
'exception_message' => $exception->getMessage(),
'exception_code' => $exception->getCode(),
]);
}
}
/**
* @throws SocialAccountTokenInvalidException
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
private function getCrmService(Models\Team $team, Models\User $user): ServiceInterface
{
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
return $crmResolver->prepareCrmService();
}
/**
* Sync CRM identifiers from a matched participant onto the activity when appropriate.
*/
private function syncActivityCrmFromParticipant(
Models\Activity $activity,
Models\Participant $participant,
LoggerInterface $logger
): void {
$hasUpdate = false;
if ($participant->getContactId() !== null && $activity->getContactId() === null) {
$logger->info('[MatchCrmObject] Syncing contact_id from participant to activity', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'from_contact_id' => $activity->getContactId(),
'to_contact_id' => $participant->getContactId(),
]);
$activity->setLeadId(null);
if ($activity->getStage() !== null && $activity->getStage()->getType() === Stage::TYPE_LEAD) {
$logger->info('[MatchCrmObject] Clearing lead stage on activity due to contact match', [
'activity_id' => $activity->getId(),
'stage_id' => $activity->getStageId(),
]);
$activity->setStageId(null);
}
$activity->setContactId($participant->getContactId());
$logger->info('[MatchCrmObject] Activity contact_id set', [
'activity_id' => $activity->getId(),
'contact_id' => $activity->getContactId(),
]);
$hasUpdate = true;
}
if ($participant->getLeadId() !== null
&& $activity->getLeadId() === null
&& $activity->getContactId() === null
&& $activity->getAccountId() === null
) {
$logger->info('[MatchCrmObject] Syncing lead_id from participant to activity', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'lead_id' => $participant->getLeadId(),
]);
$activity->setLeadId($participant->getLeadId());
$hasUpdate = true;
}
if ($hasUpdate) {
$activity->save();
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preference' => 'boolean',
'recording_reason_code' => 'integer',
'has_recording_prompt' => 'boolean',
'on_air' => 'integer',
];
}
protected static function boot()
{
parent::boot();
static::updated(static function (Activity $activity) {
// If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week
if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||
($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {
if ($activity->isDirty('status')) {
event(new StatusUpdated($activity));
}
if ($activity->isDirty('stage_id')) {
event(new StageUpdated($activity));
}
if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {
event(new ProspectUpdated($activity));
}
if ($activity->isDirty('opportunity_id')) {
event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));
}
if ($activity->isDirty('title')) {
event(new TitleUpdated($activity));
}
}
if ($activity->isDirty('playbook_category_id')) {
event(new ActivityTypeUpdated($activity));
}
});
static:...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.12566489,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.37400267,"top":0.10055866,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"9","depth":4,"bounds":{"left":0.38397607,"top":0.10055866,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.39361703,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.40093085,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\Activities\\Crm;\n\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Events\\Activities\\Participants\\Joined;\nuse Jiminny\\Events\\Activities\\Connections\\Opened;\nuse Jiminny\\Exceptions\\HttpSessionExpiredException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Jiminny\\Services\\Crm\\ProspectSearchStrategyFactory;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\n\nclass MatchCrmObject implements ShouldQueue\n{\n /**\n * Match the remote CRM relations to the participant as they join.\n */\n public function handle(Opened|Joined $event): void\n {\n $emailHelper = app(EmailHelper::class);\n $teamRepository = app(TeamRepository::class);\n $logger = app(LoggerInterface::class);\n\n $participant = $event->getParticipant();\n $activity = $event->getActivity();\n\n // Don't bother if they are a team member.\n if ($participant->isTeamMember()) {\n $logger->info('[MatchCrmObject] Participant is team member');\n\n return;\n }\n\n $user = $activity->getUser();\n $team = $user->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $logger->info('[MatchCrmObject] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n $email = $participant->getEmailAddress();\n if ($email !== null\n && ! $emailHelper->shouldPerformLookup($team, $email)\n ) {\n $logger->info('[MatchCrmObject] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $email,\n ]);\n\n $teamMember = $teamRepository->findActiveTeamMemberByEmail($team, $email);\n if ($teamMember instanceof Models\\User) {\n $participant->setUserId($teamMember->getId());\n $participant->save();\n }\n\n return;\n }\n\n // If we have previously matched their crm identity,\n // sync the crm object to get the freshest state, then skip any further lookup\n if ($participant->getLeadId() !== null || $participant->getContactId() !== null) {\n $this->syncActivityCrmFromParticipant($activity, $participant, $logger);\n\n $logger->info('[MatchCrmObject] Participant already has a crm prospect identified, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'email' => $email,\n ]);\n\n return;\n }\n\n try {\n $crmService = $this->getCrmService($team, $user);\n\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $records = null;\n\n if ($email !== null) {\n $records = $decorator->matchExactlyByEmail($email, $user->getId());\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $decorator->matchByPhone($participant->getPhoneNumber(), null, $user->getId());\n }\n\n if (empty($records) && $email !== null) {\n $records = $decorator->matchByDomain($email, $user->getId());\n }\n\n if (! empty($records)) {\n $activity->updateParticipantCrmData($records, $participant);\n $activity->updateActivityCrmData($records);\n }\n } catch (HttpSessionExpiredException|SocialAccountTokenInvalidException $exception) {\n $logger->info('[MatchCrmObject] Activity user triggered exception', [\n 'user_id' => $activity->getUserId(),\n 'activity_id' => $activity->getId(),\n 'exception_message' => $exception->getMessage(),\n 'exception_code' => $exception->getCode(),\n ]);\n }\n }\n\n /**\n * @throws SocialAccountTokenInvalidException\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n */\n private function getCrmService(Models\\Team $team, Models\\User $user): ServiceInterface\n {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n\n return $crmResolver->prepareCrmService();\n }\n\n /**\n * Sync CRM identifiers from a matched participant onto the activity when appropriate.\n */\n private function syncActivityCrmFromParticipant(\n Models\\Activity $activity,\n Models\\Participant $participant,\n LoggerInterface $logger\n ): void {\n $hasUpdate = false;\n\n if ($participant->getContactId() !== null && $activity->getContactId() === null) {\n $logger->info('[MatchCrmObject] Syncing contact_id from participant to activity', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'from_contact_id' => $activity->getContactId(),\n 'to_contact_id' => $participant->getContactId(),\n ]);\n\n $activity->setLeadId(null);\n\n if ($activity->getStage() !== null && $activity->getStage()->getType() === Stage::TYPE_LEAD) {\n $logger->info('[MatchCrmObject] Clearing lead stage on activity due to contact match', [\n 'activity_id' => $activity->getId(),\n 'stage_id' => $activity->getStageId(),\n ]);\n $activity->setStageId(null);\n }\n\n $activity->setContactId($participant->getContactId());\n\n $logger->info('[MatchCrmObject] Activity contact_id set', [\n 'activity_id' => $activity->getId(),\n 'contact_id' => $activity->getContactId(),\n ]);\n\n $hasUpdate = true;\n }\n\n if ($participant->getLeadId() !== null\n && $activity->getLeadId() === null\n && $activity->getContactId() === null\n && $activity->getAccountId() === null\n ) {\n $logger->info('[MatchCrmObject] Syncing lead_id from participant to activity', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'lead_id' => $participant->getLeadId(),\n ]);\n\n $activity->setLeadId($participant->getLeadId());\n $hasUpdate = true;\n }\n\n if ($hasUpdate) {\n $activity->save();\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\Activities\\Crm;\n\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Events\\Activities\\Participants\\Joined;\nuse Jiminny\\Events\\Activities\\Connections\\Opened;\nuse Jiminny\\Exceptions\\HttpSessionExpiredException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Jiminny\\Services\\Crm\\ProspectSearchStrategyFactory;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\n\nclass MatchCrmObject implements ShouldQueue\n{\n /**\n * Match the remote CRM relations to the participant as they join.\n */\n public function handle(Opened|Joined $event): void\n {\n $emailHelper = app(EmailHelper::class);\n $teamRepository = app(TeamRepository::class);\n $logger = app(LoggerInterface::class);\n\n $participant = $event->getParticipant();\n $activity = $event->getActivity();\n\n // Don't bother if they are a team member.\n if ($participant->isTeamMember()) {\n $logger->info('[MatchCrmObject] Participant is team member');\n\n return;\n }\n\n $user = $activity->getUser();\n $team = $user->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $logger->info('[MatchCrmObject] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n $email = $participant->getEmailAddress();\n if ($email !== null\n && ! $emailHelper->shouldPerformLookup($team, $email)\n ) {\n $logger->info('[MatchCrmObject] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $email,\n ]);\n\n $teamMember = $teamRepository->findActiveTeamMemberByEmail($team, $email);\n if ($teamMember instanceof Models\\User) {\n $participant->setUserId($teamMember->getId());\n $participant->save();\n }\n\n return;\n }\n\n // If we have previously matched their crm identity,\n // sync the crm object to get the freshest state, then skip any further lookup\n if ($participant->getLeadId() !== null || $participant->getContactId() !== null) {\n $this->syncActivityCrmFromParticipant($activity, $participant, $logger);\n\n $logger->info('[MatchCrmObject] Participant already has a crm prospect identified, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'email' => $email,\n ]);\n\n return;\n }\n\n try {\n $crmService = $this->getCrmService($team, $user);\n\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $records = null;\n\n if ($email !== null) {\n $records = $decorator->matchExactlyByEmail($email, $user->getId());\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $decorator->matchByPhone($participant->getPhoneNumber(), null, $user->getId());\n }\n\n if (empty($records) && $email !== null) {\n $records = $decorator->matchByDomain($email, $user->getId());\n }\n\n if (! empty($records)) {\n $activity->updateParticipantCrmData($records, $participant);\n $activity->updateActivityCrmData($records);\n }\n } catch (HttpSessionExpiredException|SocialAccountTokenInvalidException $exception) {\n $logger->info('[MatchCrmObject] Activity user triggered exception', [\n 'user_id' => $activity->getUserId(),\n 'activity_id' => $activity->getId(),\n 'exception_message' => $exception->getMessage(),\n 'exception_code' => $exception->getCode(),\n ]);\n }\n }\n\n /**\n * @throws SocialAccountTokenInvalidException\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n */\n private function getCrmService(Models\\Team $team, Models\\User $user): ServiceInterface\n {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n\n return $crmResolver->prepareCrmService();\n }\n\n /**\n * Sync CRM identifiers from a matched participant onto the activity when appropriate.\n */\n private function syncActivityCrmFromParticipant(\n Models\\Activity $activity,\n Models\\Participant $participant,\n LoggerInterface $logger\n ): void {\n $hasUpdate = false;\n\n if ($participant->getContactId() !== null && $activity->getContactId() === null) {\n $logger->info('[MatchCrmObject] Syncing contact_id from participant to activity', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'from_contact_id' => $activity->getContactId(),\n 'to_contact_id' => $participant->getContactId(),\n ]);\n\n $activity->setLeadId(null);\n\n if ($activity->getStage() !== null && $activity->getStage()->getType() === Stage::TYPE_LEAD) {\n $logger->info('[MatchCrmObject] Clearing lead stage on activity due to contact match', [\n 'activity_id' => $activity->getId(),\n 'stage_id' => $activity->getStageId(),\n ]);\n $activity->setStageId(null);\n }\n\n $activity->setContactId($participant->getContactId());\n\n $logger->info('[MatchCrmObject] Activity contact_id set', [\n 'activity_id' => $activity->getId(),\n 'contact_id' => $activity->getContactId(),\n ]);\n\n $hasUpdate = true;\n }\n\n if ($participant->getLeadId() !== null\n && $activity->getLeadId() === null\n && $activity->getContactId() === null\n && $activity->getAccountId() === null\n ) {\n $logger->info('[MatchCrmObject] Syncing lead_id from participant to activity', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'lead_id' => $participant->getLeadId(),\n ]);\n\n $activity->setLeadId($participant->getLeadId());\n $hasUpdate = true;\n }\n\n if ($hasUpdate) {\n $activity->save();\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.6665558,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"169","depth":4,"bounds":{"left":0.6765292,"top":0.12529927,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.69049203,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"102","depth":4,"bounds":{"left":0.70046544,"top":0.12529927,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.7144282,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.7240692,"top":0.123703115,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.73138297,"top":0.123703115,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
7943249111508506828
|
7035618767472895804
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\Activities\Crm;
use Illuminate\Contracts\Queue\ShouldQueue;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Events\Activities\Participants\Joined;
use Jiminny\Events\Activities\Connections\Opened;
use Jiminny\Exceptions\HttpSessionExpiredException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models;
use Jiminny\Models\Stage;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Jiminny\Services\Crm\ProspectSearchStrategyFactory;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
class MatchCrmObject implements ShouldQueue
{
/**
* Match the remote CRM relations to the participant as they join.
*/
public function handle(Opened|Joined $event): void
{
$emailHelper = app(EmailHelper::class);
$teamRepository = app(TeamRepository::class);
$logger = app(LoggerInterface::class);
$participant = $event->getParticipant();
$activity = $event->getActivity();
// Don't bother if they are a team member.
if ($participant->isTeamMember()) {
$logger->info('[MatchCrmObject] Participant is team member');
return;
}
$user = $activity->getUser();
$team = $user->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$logger->info('[MatchCrmObject] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
$email = $participant->getEmailAddress();
if ($email !== null
&& ! $emailHelper->shouldPerformLookup($team, $email)
) {
$logger->info('[MatchCrmObject] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $email,
]);
$teamMember = $teamRepository->findActiveTeamMemberByEmail($team, $email);
if ($teamMember instanceof Models\User) {
$participant->setUserId($teamMember->getId());
$participant->save();
}
return;
}
// If we have previously matched their crm identity,
// sync the crm object to get the freshest state, then skip any further lookup
if ($participant->getLeadId() !== null || $participant->getContactId() !== null) {
$this->syncActivityCrmFromParticipant($activity, $participant, $logger);
$logger->info('[MatchCrmObject] Participant already has a crm prospect identified, skipping crm lookup', [
'activity_id' => $activity->getId(),
'email' => $email,
]);
return;
}
try {
$crmService = $this->getCrmService($team, $user);
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$records = null;
if ($email !== null) {
$records = $decorator->matchExactlyByEmail($email, $user->getId());
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $decorator->matchByPhone($participant->getPhoneNumber(), null, $user->getId());
}
if (empty($records) && $email !== null) {
$records = $decorator->matchByDomain($email, $user->getId());
}
if (! empty($records)) {
$activity->updateParticipantCrmData($records, $participant);
$activity->updateActivityCrmData($records);
}
} catch (HttpSessionExpiredException|SocialAccountTokenInvalidException $exception) {
$logger->info('[MatchCrmObject] Activity user triggered exception', [
'user_id' => $activity->getUserId(),
'activity_id' => $activity->getId(),
'exception_message' => $exception->getMessage(),
'exception_code' => $exception->getCode(),
]);
}
}
/**
* @throws SocialAccountTokenInvalidException
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
private function getCrmService(Models\Team $team, Models\User $user): ServiceInterface
{
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
return $crmResolver->prepareCrmService();
}
/**
* Sync CRM identifiers from a matched participant onto the activity when appropriate.
*/
private function syncActivityCrmFromParticipant(
Models\Activity $activity,
Models\Participant $participant,
LoggerInterface $logger
): void {
$hasUpdate = false;
if ($participant->getContactId() !== null && $activity->getContactId() === null) {
$logger->info('[MatchCrmObject] Syncing contact_id from participant to activity', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'from_contact_id' => $activity->getContactId(),
'to_contact_id' => $participant->getContactId(),
]);
$activity->setLeadId(null);
if ($activity->getStage() !== null && $activity->getStage()->getType() === Stage::TYPE_LEAD) {
$logger->info('[MatchCrmObject] Clearing lead stage on activity due to contact match', [
'activity_id' => $activity->getId(),
'stage_id' => $activity->getStageId(),
]);
$activity->setStageId(null);
}
$activity->setContactId($participant->getContactId());
$logger->info('[MatchCrmObject] Activity contact_id set', [
'activity_id' => $activity->getId(),
'contact_id' => $activity->getContactId(),
]);
$hasUpdate = true;
}
if ($participant->getLeadId() !== null
&& $activity->getLeadId() === null
&& $activity->getContactId() === null
&& $activity->getAccountId() === null
) {
$logger->info('[MatchCrmObject] Syncing lead_id from participant to activity', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'lead_id' => $participant->getLeadId(),
]);
$activity->setLeadId($participant->getLeadId());
$hasUpdate = true;
}
if ($hasUpdate) {
$activity->save();
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preference' => 'boolean',
'recording_reason_code' => 'integer',
'has_recording_prompt' => 'boolean',
'on_air' => 'integer',
];
}
protected static function boot()
{
parent::boot();
static::updated(static function (Activity $activity) {
// If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week
if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||
($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {
if ($activity->isDirty('status')) {
event(new StatusUpdated($activity));
}
if ($activity->isDirty('stage_id')) {
event(new StageUpdated($activity));
}
if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {
event(new ProspectUpdated($activity));
}
if ($activity->isDirty('opportunity_id')) {
event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));
}
if ($activity->isDirty('title')) {
event(new TitleUpdated($activity));
}
}
if ($activity->isDirty('playbook_category_id')) {
event(new ActivityTypeUpdated($activity));
}
});
static:...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
43044
|
1571
|
53
|
2026-05-14T12:15:42.370521+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760942370_m1.jpg...
|
PhpStorm
|
faVsco.js – Activity.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\Activities\Crm;
use Illuminate\Contracts\Queue\ShouldQueue;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Events\Activities\Participants\Joined;
use Jiminny\Events\Activities\Connections\Opened;
use Jiminny\Exceptions\HttpSessionExpiredException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models;
use Jiminny\Models\Stage;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Jiminny\Services\Crm\ProspectSearchStrategyFactory;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
class MatchCrmObject implements ShouldQueue
{
/**
* Match the remote CRM relations to the participant as they join.
*/
public function handle(Opened|Joined $event): void
{
$emailHelper = app(EmailHelper::class);
$teamRepository = app(TeamRepository::class);
$logger = app(LoggerInterface::class);
$participant = $event->getParticipant();
$activity = $event->getActivity();
// Don't bother if they are a team member.
if ($participant->isTeamMember()) {
$logger->info('[MatchCrmObject] Participant is team member');
return;
}
$user = $activity->getUser();
$team = $user->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$logger->info('[MatchCrmObject] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
$email = $participant->getEmailAddress();
if ($email !== null
&& ! $emailHelper->shouldPerformLookup($team, $email)
) {
$logger->info('[MatchCrmObject] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $email,
]);
$teamMember = $teamRepository->findActiveTeamMemberByEmail($team, $email);
if ($teamMember instanceof Models\User) {
$participant->setUserId($teamMember->getId());
$participant->save();
}
return;
}
// If we have previously matched their crm identity,
// sync the crm object to get the freshest state, then skip any further lookup
if ($participant->getLeadId() !== null || $participant->getContactId() !== null) {
$this->syncActivityCrmFromParticipant($activity, $participant, $logger);
$logger->info('[MatchCrmObject] Participant already has a crm prospect identified, skipping crm lookup', [
'activity_id' => $activity->getId(),
'email' => $email,
]);
return;
}
try {
$crmService = $this->getCrmService($team, $user);
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$records = null;
if ($email !== null) {
$records = $decorator->matchExactlyByEmail($email, $user->getId());
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $decorator->matchByPhone($participant->getPhoneNumber(), null, $user->getId());
}
if (empty($records) && $email !== null) {
$records = $decorator->matchByDomain($email, $user->getId());
}
if (! empty($records)) {
$activity->updateParticipantCrmData($records, $participant);
$activity->updateActivityCrmData($records);
}
} catch (HttpSessionExpiredException|SocialAccountTokenInvalidException $exception) {
$logger->info('[MatchCrmObject] Activity user triggered exception', [
'user_id' => $activity->getUserId(),
'activity_id' => $activity->getId(),
'exception_message' => $exception->getMessage(),
'exception_code' => $exception->getCode(),
]);
}
}
/**
* @throws SocialAccountTokenInvalidException
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
private function getCrmService(Models\Team $team, Models\User $user): ServiceInterface
{
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
return $crmResolver->prepareCrmService();
}
/**
* Sync CRM identifiers from a matched participant onto the activity when appropriate.
*/
private function syncActivityCrmFromParticipant(
Models\Activity $activity,
Models\Participant $participant,
LoggerInterface $logger
): void {
$hasUpdate = false;
if ($participant->getContactId() !== null && $activity->getContactId() === null) {
$logger->info('[MatchCrmObject] Syncing contact_id from participant to activity', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'from_contact_id' => $activity->getContactId(),
'to_contact_id' => $participant->getContactId(),
]);
$activity->setLeadId(null);
if ($activity->getStage() !== null && $activity->getStage()->getType() === Stage::TYPE_LEAD) {
$logger->info('[MatchCrmObject] Clearing lead stage on activity due to contact match', [
'activity_id' => $activity->getId(),
'stage_id' => $activity->getStageId(),
]);
$activity->setStageId(null);
}
$activity->setContactId($participant->getContactId());
$logger->info('[MatchCrmObject] Activity contact_id set', [
'activity_id' => $activity->getId(),
'contact_id' => $activity->getContactId(),
]);
$hasUpdate = true;
}
if ($participant->getLeadId() !== null
&& $activity->getLeadId() === null
&& $activity->getContactId() === null
&& $activity->getAccountId() === null
) {
$logger->info('[MatchCrmObject] Syncing lead_id from participant to activity', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'lead_id' => $participant->getLeadId(),
]);
$activity->setLeadId($participant->getLeadId());
$hasUpdate = true;
}
if ($hasUpdate) {
$activity->save();
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preference' => 'boolean',
'recording_reason_code' => 'integer',
'has_recording_prompt' => 'boolean',
'on_air' => 'integer',
];
}
protected static function boot()
{
parent::boot();
static::updated(static function (Activity $activity) {
// If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week
if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||
($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {
if ($activity->isDirty('status')) {
event(new StatusUpdated($activity));
}
if ($activity->isDirty('stage_id')) {
event(new StageUpdated($activity));
}
if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {
event(new ProspectUpdated($activity));
}
if ($activity->isDirty('opportunity_id')) {
event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));
}
if ($activity->isDirty('title')) {
event(new TitleUpdated($activity));
}
}
if ($activity->isDirty('playbook_category_id')) {
event(new ActivityTypeUpdated($activity));
}
});
static:...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"9","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\Activities\\Crm;\n\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Events\\Activities\\Participants\\Joined;\nuse Jiminny\\Events\\Activities\\Connections\\Opened;\nuse Jiminny\\Exceptions\\HttpSessionExpiredException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Jiminny\\Services\\Crm\\ProspectSearchStrategyFactory;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\n\nclass MatchCrmObject implements ShouldQueue\n{\n /**\n * Match the remote CRM relations to the participant as they join.\n */\n public function handle(Opened|Joined $event): void\n {\n $emailHelper = app(EmailHelper::class);\n $teamRepository = app(TeamRepository::class);\n $logger = app(LoggerInterface::class);\n\n $participant = $event->getParticipant();\n $activity = $event->getActivity();\n\n // Don't bother if they are a team member.\n if ($participant->isTeamMember()) {\n $logger->info('[MatchCrmObject] Participant is team member');\n\n return;\n }\n\n $user = $activity->getUser();\n $team = $user->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $logger->info('[MatchCrmObject] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n $email = $participant->getEmailAddress();\n if ($email !== null\n && ! $emailHelper->shouldPerformLookup($team, $email)\n ) {\n $logger->info('[MatchCrmObject] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $email,\n ]);\n\n $teamMember = $teamRepository->findActiveTeamMemberByEmail($team, $email);\n if ($teamMember instanceof Models\\User) {\n $participant->setUserId($teamMember->getId());\n $participant->save();\n }\n\n return;\n }\n\n // If we have previously matched their crm identity,\n // sync the crm object to get the freshest state, then skip any further lookup\n if ($participant->getLeadId() !== null || $participant->getContactId() !== null) {\n $this->syncActivityCrmFromParticipant($activity, $participant, $logger);\n\n $logger->info('[MatchCrmObject] Participant already has a crm prospect identified, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'email' => $email,\n ]);\n\n return;\n }\n\n try {\n $crmService = $this->getCrmService($team, $user);\n\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $records = null;\n\n if ($email !== null) {\n $records = $decorator->matchExactlyByEmail($email, $user->getId());\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $decorator->matchByPhone($participant->getPhoneNumber(), null, $user->getId());\n }\n\n if (empty($records) && $email !== null) {\n $records = $decorator->matchByDomain($email, $user->getId());\n }\n\n if (! empty($records)) {\n $activity->updateParticipantCrmData($records, $participant);\n $activity->updateActivityCrmData($records);\n }\n } catch (HttpSessionExpiredException|SocialAccountTokenInvalidException $exception) {\n $logger->info('[MatchCrmObject] Activity user triggered exception', [\n 'user_id' => $activity->getUserId(),\n 'activity_id' => $activity->getId(),\n 'exception_message' => $exception->getMessage(),\n 'exception_code' => $exception->getCode(),\n ]);\n }\n }\n\n /**\n * @throws SocialAccountTokenInvalidException\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n */\n private function getCrmService(Models\\Team $team, Models\\User $user): ServiceInterface\n {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n\n return $crmResolver->prepareCrmService();\n }\n\n /**\n * Sync CRM identifiers from a matched participant onto the activity when appropriate.\n */\n private function syncActivityCrmFromParticipant(\n Models\\Activity $activity,\n Models\\Participant $participant,\n LoggerInterface $logger\n ): void {\n $hasUpdate = false;\n\n if ($participant->getContactId() !== null && $activity->getContactId() === null) {\n $logger->info('[MatchCrmObject] Syncing contact_id from participant to activity', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'from_contact_id' => $activity->getContactId(),\n 'to_contact_id' => $participant->getContactId(),\n ]);\n\n $activity->setLeadId(null);\n\n if ($activity->getStage() !== null && $activity->getStage()->getType() === Stage::TYPE_LEAD) {\n $logger->info('[MatchCrmObject] Clearing lead stage on activity due to contact match', [\n 'activity_id' => $activity->getId(),\n 'stage_id' => $activity->getStageId(),\n ]);\n $activity->setStageId(null);\n }\n\n $activity->setContactId($participant->getContactId());\n\n $logger->info('[MatchCrmObject] Activity contact_id set', [\n 'activity_id' => $activity->getId(),\n 'contact_id' => $activity->getContactId(),\n ]);\n\n $hasUpdate = true;\n }\n\n if ($participant->getLeadId() !== null\n && $activity->getLeadId() === null\n && $activity->getContactId() === null\n && $activity->getAccountId() === null\n ) {\n $logger->info('[MatchCrmObject] Syncing lead_id from participant to activity', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'lead_id' => $participant->getLeadId(),\n ]);\n\n $activity->setLeadId($participant->getLeadId());\n $hasUpdate = true;\n }\n\n if ($hasUpdate) {\n $activity->save();\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\Activities\\Crm;\n\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Events\\Activities\\Participants\\Joined;\nuse Jiminny\\Events\\Activities\\Connections\\Opened;\nuse Jiminny\\Exceptions\\HttpSessionExpiredException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Jiminny\\Services\\Crm\\ProspectSearchStrategyFactory;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\n\nclass MatchCrmObject implements ShouldQueue\n{\n /**\n * Match the remote CRM relations to the participant as they join.\n */\n public function handle(Opened|Joined $event): void\n {\n $emailHelper = app(EmailHelper::class);\n $teamRepository = app(TeamRepository::class);\n $logger = app(LoggerInterface::class);\n\n $participant = $event->getParticipant();\n $activity = $event->getActivity();\n\n // Don't bother if they are a team member.\n if ($participant->isTeamMember()) {\n $logger->info('[MatchCrmObject] Participant is team member');\n\n return;\n }\n\n $user = $activity->getUser();\n $team = $user->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $logger->info('[MatchCrmObject] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n $email = $participant->getEmailAddress();\n if ($email !== null\n && ! $emailHelper->shouldPerformLookup($team, $email)\n ) {\n $logger->info('[MatchCrmObject] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $email,\n ]);\n\n $teamMember = $teamRepository->findActiveTeamMemberByEmail($team, $email);\n if ($teamMember instanceof Models\\User) {\n $participant->setUserId($teamMember->getId());\n $participant->save();\n }\n\n return;\n }\n\n // If we have previously matched their crm identity,\n // sync the crm object to get the freshest state, then skip any further lookup\n if ($participant->getLeadId() !== null || $participant->getContactId() !== null) {\n $this->syncActivityCrmFromParticipant($activity, $participant, $logger);\n\n $logger->info('[MatchCrmObject] Participant already has a crm prospect identified, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'email' => $email,\n ]);\n\n return;\n }\n\n try {\n $crmService = $this->getCrmService($team, $user);\n\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $records = null;\n\n if ($email !== null) {\n $records = $decorator->matchExactlyByEmail($email, $user->getId());\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $decorator->matchByPhone($participant->getPhoneNumber(), null, $user->getId());\n }\n\n if (empty($records) && $email !== null) {\n $records = $decorator->matchByDomain($email, $user->getId());\n }\n\n if (! empty($records)) {\n $activity->updateParticipantCrmData($records, $participant);\n $activity->updateActivityCrmData($records);\n }\n } catch (HttpSessionExpiredException|SocialAccountTokenInvalidException $exception) {\n $logger->info('[MatchCrmObject] Activity user triggered exception', [\n 'user_id' => $activity->getUserId(),\n 'activity_id' => $activity->getId(),\n 'exception_message' => $exception->getMessage(),\n 'exception_code' => $exception->getCode(),\n ]);\n }\n }\n\n /**\n * @throws SocialAccountTokenInvalidException\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n */\n private function getCrmService(Models\\Team $team, Models\\User $user): ServiceInterface\n {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n\n return $crmResolver->prepareCrmService();\n }\n\n /**\n * Sync CRM identifiers from a matched participant onto the activity when appropriate.\n */\n private function syncActivityCrmFromParticipant(\n Models\\Activity $activity,\n Models\\Participant $participant,\n LoggerInterface $logger\n ): void {\n $hasUpdate = false;\n\n if ($participant->getContactId() !== null && $activity->getContactId() === null) {\n $logger->info('[MatchCrmObject] Syncing contact_id from participant to activity', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'from_contact_id' => $activity->getContactId(),\n 'to_contact_id' => $participant->getContactId(),\n ]);\n\n $activity->setLeadId(null);\n\n if ($activity->getStage() !== null && $activity->getStage()->getType() === Stage::TYPE_LEAD) {\n $logger->info('[MatchCrmObject] Clearing lead stage on activity due to contact match', [\n 'activity_id' => $activity->getId(),\n 'stage_id' => $activity->getStageId(),\n ]);\n $activity->setStageId(null);\n }\n\n $activity->setContactId($participant->getContactId());\n\n $logger->info('[MatchCrmObject] Activity contact_id set', [\n 'activity_id' => $activity->getId(),\n 'contact_id' => $activity->getContactId(),\n ]);\n\n $hasUpdate = true;\n }\n\n if ($participant->getLeadId() !== null\n && $activity->getLeadId() === null\n && $activity->getContactId() === null\n && $activity->getAccountId() === null\n ) {\n $logger->info('[MatchCrmObject] Syncing lead_id from participant to activity', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'lead_id' => $participant->getLeadId(),\n ]);\n\n $activity->setLeadId($participant->getLeadId());\n $hasUpdate = true;\n }\n\n if ($hasUpdate) {\n $activity->save();\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"169","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"102","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
7943249111508506828
|
7035618767472895804
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\Activities\Crm;
use Illuminate\Contracts\Queue\ShouldQueue;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Events\Activities\Participants\Joined;
use Jiminny\Events\Activities\Connections\Opened;
use Jiminny\Exceptions\HttpSessionExpiredException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models;
use Jiminny\Models\Stage;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Jiminny\Services\Crm\ProspectSearchStrategyFactory;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
class MatchCrmObject implements ShouldQueue
{
/**
* Match the remote CRM relations to the participant as they join.
*/
public function handle(Opened|Joined $event): void
{
$emailHelper = app(EmailHelper::class);
$teamRepository = app(TeamRepository::class);
$logger = app(LoggerInterface::class);
$participant = $event->getParticipant();
$activity = $event->getActivity();
// Don't bother if they are a team member.
if ($participant->isTeamMember()) {
$logger->info('[MatchCrmObject] Participant is team member');
return;
}
$user = $activity->getUser();
$team = $user->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$logger->info('[MatchCrmObject] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
$email = $participant->getEmailAddress();
if ($email !== null
&& ! $emailHelper->shouldPerformLookup($team, $email)
) {
$logger->info('[MatchCrmObject] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $email,
]);
$teamMember = $teamRepository->findActiveTeamMemberByEmail($team, $email);
if ($teamMember instanceof Models\User) {
$participant->setUserId($teamMember->getId());
$participant->save();
}
return;
}
// If we have previously matched their crm identity,
// sync the crm object to get the freshest state, then skip any further lookup
if ($participant->getLeadId() !== null || $participant->getContactId() !== null) {
$this->syncActivityCrmFromParticipant($activity, $participant, $logger);
$logger->info('[MatchCrmObject] Participant already has a crm prospect identified, skipping crm lookup', [
'activity_id' => $activity->getId(),
'email' => $email,
]);
return;
}
try {
$crmService = $this->getCrmService($team, $user);
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$records = null;
if ($email !== null) {
$records = $decorator->matchExactlyByEmail($email, $user->getId());
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $decorator->matchByPhone($participant->getPhoneNumber(), null, $user->getId());
}
if (empty($records) && $email !== null) {
$records = $decorator->matchByDomain($email, $user->getId());
}
if (! empty($records)) {
$activity->updateParticipantCrmData($records, $participant);
$activity->updateActivityCrmData($records);
}
} catch (HttpSessionExpiredException|SocialAccountTokenInvalidException $exception) {
$logger->info('[MatchCrmObject] Activity user triggered exception', [
'user_id' => $activity->getUserId(),
'activity_id' => $activity->getId(),
'exception_message' => $exception->getMessage(),
'exception_code' => $exception->getCode(),
]);
}
}
/**
* @throws SocialAccountTokenInvalidException
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
private function getCrmService(Models\Team $team, Models\User $user): ServiceInterface
{
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
return $crmResolver->prepareCrmService();
}
/**
* Sync CRM identifiers from a matched participant onto the activity when appropriate.
*/
private function syncActivityCrmFromParticipant(
Models\Activity $activity,
Models\Participant $participant,
LoggerInterface $logger
): void {
$hasUpdate = false;
if ($participant->getContactId() !== null && $activity->getContactId() === null) {
$logger->info('[MatchCrmObject] Syncing contact_id from participant to activity', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'from_contact_id' => $activity->getContactId(),
'to_contact_id' => $participant->getContactId(),
]);
$activity->setLeadId(null);
if ($activity->getStage() !== null && $activity->getStage()->getType() === Stage::TYPE_LEAD) {
$logger->info('[MatchCrmObject] Clearing lead stage on activity due to contact match', [
'activity_id' => $activity->getId(),
'stage_id' => $activity->getStageId(),
]);
$activity->setStageId(null);
}
$activity->setContactId($participant->getContactId());
$logger->info('[MatchCrmObject] Activity contact_id set', [
'activity_id' => $activity->getId(),
'contact_id' => $activity->getContactId(),
]);
$hasUpdate = true;
}
if ($participant->getLeadId() !== null
&& $activity->getLeadId() === null
&& $activity->getContactId() === null
&& $activity->getAccountId() === null
) {
$logger->info('[MatchCrmObject] Syncing lead_id from participant to activity', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'lead_id' => $participant->getLeadId(),
]);
$activity->setLeadId($participant->getLeadId());
$hasUpdate = true;
}
if ($hasUpdate) {
$activity->save();
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preference' => 'boolean',
'recording_reason_code' => 'integer',
'has_recording_prompt' => 'boolean',
'on_air' => 'integer',
];
}
protected static function boot()
{
parent::boot();
static::updated(static function (Activity $activity) {
// If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week
if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||
($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {
if ($activity->isDirty('status')) {
event(new StatusUpdated($activity));
}
if ($activity->isDirty('stage_id')) {
event(new StageUpdated($activity));
}
if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {
event(new ProspectUpdated($activity));
}
if ($activity->isDirty('opportunity_id')) {
event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));
}
if ($activity->isDirty('title')) {
event(new TitleUpdated($activity));
}
}
if ($activity->isDirty('playbook_category_id')) {
event(new ActivityTypeUpdated($activity));
}
});
static:...
|
43043
|
NULL
|
NULL
|
NULL
|
|
43043
|
1571
|
52
|
2026-05-14T12:15:37.906434+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760937906_m1.jpg...
|
PhpStorm
|
faVsco.js – Activity.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
SlackFileEditViewGoHistoryWindowHomeDMsActivityFil SlackFileEditViewGoHistoryWindowHomeDMsActivityFilesLaterMoreJiminny ...# general# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi.Direct messagesP. Galya Dimitrova5. Vasil VasilevR. Aneliya AngelovaStefka StoyanovaTodor StamatovMario Georgiev. Nikolay Ivanovdo James Graham2. Stoyan TanevSteliyan Georgiev&. Petko KashinskiLukas Kovalik y...AppsToastJira CloudGoogle Cale... &sHelp$ 0lhl→Describe what you are looking forVasil VasilevMessagesAdd canvasO Files& Pins +ние имаме някаква история на сделката, имаме стейдж по време наToday ~на активититоно тоя стейдж го ползваме само за това тьрсене в on demand (edited)преди малко подхвърлих иначе на Галя идеята, дали да не ги покажем смените на стейджовете на сделките в deal insightsимаме дата на отваряне и затваряне на сделказащо да не сложим и един маркер кога се е променил стейджатака в таймлайна ще се вижда "развитието" на тая сделкаLukas Kovalik 2:45 PMхм ами то дали да го няма вечеVasil Vasilev 2:45 PMняма гоLukas Kovalik 2:46 PMняма дано ВЕ си го врьщас тази цел точноVasil Vasilev 2:47 PMзнам, проверих, преди да предложа на Галя да ги покажемвЪЗМоЖНоно явно не е направеноVasil Vasilev 3:04 PMв крайна сметка тоя метод Activity:updateActivityCrmData се вика само при подмяна на opportunity, нали така?Lukas Kovalik 3:05 PMгледам го сегаMessage Vasil Vasilev+100% <8• Thu 14 May 15:15:37...
|
NULL
|
8375282397840646681
|
NULL
|
click
|
ocr
|
NULL
|
SlackFileEditViewGoHistoryWindowHomeDMsActivityFil SlackFileEditViewGoHistoryWindowHomeDMsActivityFilesLaterMoreJiminny ...# general# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi.Direct messagesP. Galya Dimitrova5. Vasil VasilevR. Aneliya AngelovaStefka StoyanovaTodor StamatovMario Georgiev. Nikolay Ivanovdo James Graham2. Stoyan TanevSteliyan Georgiev&. Petko KashinskiLukas Kovalik y...AppsToastJira CloudGoogle Cale... &sHelp$ 0lhl→Describe what you are looking forVasil VasilevMessagesAdd canvasO Files& Pins +ние имаме някаква история на сделката, имаме стейдж по време наToday ~на активититоно тоя стейдж го ползваме само за това тьрсене в on demand (edited)преди малко подхвърлих иначе на Галя идеята, дали да не ги покажем смените на стейджовете на сделките в deal insightsимаме дата на отваряне и затваряне на сделказащо да не сложим и един маркер кога се е променил стейджатака в таймлайна ще се вижда "развитието" на тая сделкаLukas Kovalik 2:45 PMхм ами то дали да го няма вечеVasil Vasilev 2:45 PMняма гоLukas Kovalik 2:46 PMняма дано ВЕ си го врьщас тази цел точноVasil Vasilev 2:47 PMзнам, проверих, преди да предложа на Галя да ги покажемвЪЗМоЖНоно явно не е направеноVasil Vasilev 3:04 PMв крайна сметка тоя метод Activity:updateActivityCrmData се вика само при подмяна на opportunity, нали така?Lukas Kovalik 3:05 PMгледам го сегаMessage Vasil Vasilev+100% <8• Thu 14 May 15:15:37...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
43042
|
1572
|
77
|
2026-05-14T12:15:35.215271+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760935215_m2.jpg...
|
PhpStorm
|
faVsco.js – Activity.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.12566489,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-2866196027416673746
|
-8132372713639400506
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
PhostormVIewINavicarecodeFV faVsco.js( #12077 on JY-20903-update_activity-stage-on...hroledeyUserinvitationdro.onpImportParticipants.php= custom.logscratch. &.ison= laravel.logA SF jiminny@localhost]& HS_local [jiminny@localhost]console [PRODI© AutoLogcancelleaAcuiv© UpdateCrmData.phpA console (EU]tiò accounts [EU]fii stages (EU]iai teams EU.ImporbotkecoraingJoo.org© Activity.php X© AutosyncActivity.ong© MatchCrmObject.phpclass nacchurmuoect Implements shoulayueue© FixActivitiesOpportunity.php© Opportunity.php& console SlAGiNGUpdatecrmbata.onp> D Following• _ ProvidersW sessions@Intercomv W Planhat© CreatesmsReceivedC) CreateSmsSentevenC) createReceived Notficac Smsuistener.ono• m SoftohonelC) DialerMetricsimoortedActiP ElasticSearchMetricsRemo(C) GenerateSxoortTokens.oh8 ImnortRemote Trackl istencc) PlanhatActivitvl istener. ohReindexForAccountListene@ PeindeyForContactl istene(e) Poindex=orGrouoListener.ReindexForLeadListener.plReindexForOpportunityList© ReindexForUserListener.pt© ResolveProvider.php© SendExportEmail.php© SetupIntegrationAction.ph© UserPilotActivityListener.p> @ AuthenticationAutomatedReportscalendars• D crmc8oorstraontearationAoo.o@ ImportActivitvTvpes.php(C)ImoortMetadata.onoC) InitProfiles.oho@LavoutModified Listener.oh 142© LayoutUpdatedListener.phC RematchActivitvencrmobil@ RemoteCrmPecordDelated 144(C) [EMAIL](C) SvncOnnortunitv.nhn146(@) SuncProfilel eads.nhnl(C) [EMAIL] DealPicks)m SinctinConrch)mCroune>C Import) M MailhaspubuIc tunccion nanale upenedJolned sevent. vo1dclass Accvicy excenas model Imolemencs84 B169 M4 M 102 24 ^if (empty($records) && $participant->getPhoneNumber() |== null) {srecords =decoraton->matchsv?honelsoartzczoant->oetPhonerumberor.rawpnoneNumber: null, Suser 2089if Cemoty(Srecords) && Semail le= nuub <Srecords = $decorator->matchByDomain(Semail, Suser->getidO):if (! empty(Srecords)) 1Sactivity->updateParticipantCrmData(Srecords, $participant):Sactivitv->undateActivitvCrmlata(Srecords):} catch (HttpSessionExpiredException|SocialAccountTokenInvalidException $exception) {$logger->info('[MatchCrm0biect] Activity user triggered exception', ['user_id' => Sactivity->getUserIdO'activity_id' => Sactivity->getIdO'exception message' => Sexception->qetMessageO."excepcion coue => sexcepcion-ceclode* dchrows soczalAccountoken.nvo zdexceotzon* ochrows contoznerzxcentzoninterrace* othrows NotFoundExcentionInterfaceprivate function getCrmService(Models\Team $team, Models \User $user): Servicelnterfacel...** Sync CRM identifiers from a matched participant onto the activity when appropriate.1 usagenrivate function svncActivitvCrmFromPanticinantdModels\Activity $activity.Models \Participant $participant,LoggerInterface $logger): void {...}20914093)2123contactnuluStage|null,strinalnul*} Srecordspublic function updateActivityCrmData(array Srecords): void// Extract the records.Slead. Saccount.$opportunity, Scontact, $stage] = Srecords;Sresolver = $this->getUpdateCrmDataResolverO:Sstrategy = Sresolver->resolveForActivity(Slead, $contact, $account):if (Sstrategy == UpdateCrmDataByStrategy::Lead) {I/ Also update the parent activity if required, checking we don't create a mixed lead/account record.if (Sthis-›account id === null && Sthis-›contact id === null && Sthis->lead id === nulb) {Sch1s->lead_ 1d = Slead->10*if (Sthis-›stage id === null && Sstage) {schis->stage 10 = Sstaqe->10*sthis->saveo:} elseif (Sstrateav== UpdateCrmDataßvStrateav::Contact)Also uodate the parent activity1f required. checking we don't create a mixed lead/account recordiif (Sthis->stage &g Sthis->stage->aetTvne() === Stage::TYPE LEAD) {sthis->stade 1d = nul.Don'+ trust orevious matched account id as it miaht have been chanaed in the CRMif (Saccount && $account->id |== $this->account_id) {Sthis->account id = Saccount->id•i* (Connontunitv Sc C+hic-Sonnontunitv id l== Connontunitv-sid) &Sthie-sonnontunitv id = Connontunitv-sid•// Reset stage id when opportunity changes to avoid stale stage from previous opportunity matchSthic-sctane id = Cctano)-sid.if (Sthis-›stage id === null && Sstage) {if (Sopportunity && Sthis->value !== Sopportunity->value) {Sthis->value = Sopportunity->value:100% L2. Thu 14 May 15:15:34U AskJiminnyReportActivityServiceTest vcascadePipedrive SDK EvaluaFixing Activity Stage DImport CRM Activity T+0 ..1.Activitv..uodateActivitvCrmData@) (lines 2122-2126)w•phpif (Sopportunity && $this->opportunity_id !== Sopportunity→>id) {// Reset stage_id when opportunity changes to avoid stale stage from previous opportunity matchschis→>stage10 = >stager→>1o;Potential Issue: If $stage is null, this will set stage_id to null even if the new opportunity has a stage in the databaseThis could happen it the ekM lookup doesn't return stage intormationRecommendation: Consider checkina if the onnortunity has a stade id and usina that as a fallback.D phpif (Sopportunity && $this->opportunity_id != Sopportunity→>id) {Sthis→>ooportunitv 1d = Sopportunity->1d:I Reset stage id when onnortunity changes to avoid stale stage from previous onportunity matchiSthis->stage id = $stage?→>id ?? Sopportunity→>stage id;However looking at the conteyt <stane ic derived from the CPM lookun recultc co ifit'c null the onnortunitv micht nothave stage information available. The current behavior is acceptable2.FixActivities0poortunitv command (lines 123-128)A nhoSactivitv-sundate(l'opportunity id' = Sopportunity->getId(),= sopportunitv->aetStageIdo).No issues found - This correctly updates both fields togetherOther locations reviewed:IntearationAoo/Service.oho (line 560): Sets stace id directlv from onnortunitvesstage id when creatina/uodatinatrom lead data - this is correct.Coov command: Copies both onnortunitv id and stage id toaether - this is correct.•ActivityController: Handles stage id from user requests - not related to the bugTest ResultsliodateGrobataTect : 2 tecte nacced (6 accortione)Matchcrmobjecttest : 15 tests passed (7o assertions)The imolementation is sound. The oniv minor concern is the null stage case in undateActivitvCrmData@. but this isacceptable given the context.• Your included weekly usage quota is exhausted. Purchase extra usage to continue using premium models. Quota resets May 17, 11:00Lets focus onl« Code SWF-1.6C0 1]WN Windsurf Toams 112-28 UTF.8io 4 spaces...
|
43040
|
NULL
|
NULL
|
NULL
|
|
43041
|
1571
|
51
|
2026-05-14T12:15:35.236879+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760935236_m1.jpg...
|
PhpStorm
|
faVsco.js – Activity.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
9
Previous Highlighted Error
Next Highlighted Error...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"9","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
3380359155758645773
|
-8672524362716173376
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
9
Previous Highlighted Error
Next Highlighted Error
SlackFileEditViewGoHistoryWindowHomeDMsActivityFilesLaterMoreJiminny ...# general# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi.Direct messagesP. Galya Dimitrova5. Vasil VasilevR. Aneliya AngelovaStefka StoyanovaTodor StamatovMario Georgiev. Nikolay Ivanovdo James Graham2. Stoyan TanevSteliyan Georgiev&. Petko KashinskiLukas Kovalik y...AppsToastJira CloudGoogle Cale... &sHelp$ 0lhl→Describe what you are looking forVasil VasilevMessagesAdd canvasO Files& Pins +ние имаме някаква история на сделката, имаме стейдж по време наToday ~на активититоно тоя стейдж го ползваме само за това тьрсене в on demand (edited)преди малко подхвърлих иначе на Галя идеята, дали да не ги покажем смените на стейджовете на сделките в deal insightsимаме дата на отваряне и затваряне на сделказащо да не сложим и един маркер кога се е променил стейджатака в таймлайна ще се вижда "развитието" на тая сделкаLukas Kovalik 2:45 PMхм ами то дали да го няма вечеVasil Vasilev 2:45 PMняма гоLukas Kovalik 2:46 PMняма дано ВЕ си го врьщас тази цел точноVasil Vasilev 2:47 PMзнам, проверих, преди да предложа на Галя да ги покажемвЪЗМоЖНоно явно не е направеноVasil Vasilev 3:04 PMв крайна сметка тоя метод Activity:updateActivityCrmData се вика само при подмяна на opportunity, нали така?Lukas Kovalik 3:05 PMгледам го сегаMessage Vasil Vasilev+100% <8• Thu 14 May 15:15:34...
|
43037
|
NULL
|
NULL
|
NULL
|
|
43040
|
1572
|
76
|
2026-05-14T12:15:26.758570+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760926758_m2.jpg...
|
PhpStorm
|
faVsco.js – MatchCrmObject.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\Activities\Crm;
use Illuminate\Contracts\Queue\ShouldQueue;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Events\Activities\Participants\Joined;
use Jiminny\Events\Activities\Connections\Opened;
use Jiminny\Exceptions\HttpSessionExpiredException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models;
use Jiminny\Models\Stage;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Jiminny\Services\Crm\ProspectSearchStrategyFactory;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
class MatchCrmObject implements ShouldQueue
{
/**
* Match the remote CRM relations to the participant as they join.
*/
public function handle(Opened|Joined $event): void
{
$emailHelper = app(EmailHelper::class);
$teamRepository = app(TeamRepository::class);
$logger = app(LoggerInterface::class);
$participant = $event->getParticipant();
$activity = $event->getActivity();
// Don't bother if they are a team member.
if ($participant->isTeamMember()) {
$logger->info('[MatchCrmObject] Participant is team member');
return;
}
$user = $activity->getUser();
$team = $user->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$logger->info('[MatchCrmObject] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
$email = $participant->getEmailAddress();
if ($email !== null
&& ! $emailHelper->shouldPerformLookup($team, $email)
) {
$logger->info('[MatchCrmObject] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $email,
]);
$teamMember = $teamRepository->findActiveTeamMemberByEmail($team, $email);
if ($teamMember instanceof Models\User) {
$participant->setUserId($teamMember->getId());
$participant->save();
}
return;
}
// If we have previously matched their crm identity,
// sync the crm object to get the freshest state, then skip any further lookup
if ($participant->getLeadId() !== null || $participant->getContactId() !== null) {
$this->syncActivityCrmFromParticipant($activity, $participant, $logger);
$logger->info('[MatchCrmObject] Participant already has a crm prospect identified, skipping crm lookup', [
'activity_id' => $activity->getId(),
'email' => $email,
]);
return;
}
try {
$crmService = $this->getCrmService($team, $user);
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$records = null;
if ($email !== null) {
$records = $decorator->matchExactlyByEmail($email, $user->getId());
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $decorator->matchByPhone($participant->getPhoneNumber(), null, $user->getId());
}
if (empty($records) && $email !== null) {
$records = $decorator->matchByDomain($email, $user->getId());
}
if (! empty($records)) {
$activity->updateParticipantCrmData($records, $participant);
$activity->updateActivityCrmData($records);
}
} catch (HttpSessionExpiredException|SocialAccountTokenInvalidException $exception) {
$logger->info('[MatchCrmObject] Activity user triggered exception', [
'user_id' => $activity->getUserId(),
'activity_id' => $activity->getId(),
'exception_message' => $exception->getMessage(),
'exception_code' => $exception->getCode(),
]);
}
}
/**
* @throws SocialAccountTokenInvalidException
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
private function getCrmService(Models\Team $team, Models\User $user): ServiceInterface
{
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
return $crmResolver->prepareCrmService();
}
/**
* Sync CRM identifiers from a matched participant onto the activity when appropriate.
*/
private function syncActivityCrmFromParticipant(
Models\Activity $activity,
Models\Participant $participant,
LoggerInterface $logger
): void {
$hasUpdate = false;
if ($participant->getContactId() !== null && $activity->getContactId() === null) {
$logger->info('[MatchCrmObject] Syncing contact_id from participant to activity', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'from_contact_id' => $activity->getContactId(),
'to_contact_id' => $participant->getContactId(),
]);
$activity->setLeadId(null);
if ($activity->getStage() !== null && $activity->getStage()->getType() === Stage::TYPE_LEAD) {
$logger->info('[MatchCrmObject] Clearing lead stage on activity due to contact match', [
'activity_id' => $activity->getId(),
'stage_id' => $activity->getStageId(),
]);
$activity->setStageId(null);
}
$activity->setContactId($participant->getContactId());
$logger->info('[MatchCrmObject] Activity contact_id set', [
'activity_id' => $activity->getId(),
'contact_id' => $activity->getContactId(),
]);
$hasUpdate = true;
}
if ($participant->getLeadId() !== null
&& $activity->getLeadId() === null
&& $activity->getContactId() === null
&& $activity->getAccountId() === null
) {
$logger->info('[MatchCrmObject] Syncing lead_id from participant to activity', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'lead_id' => $participant->getLeadId(),
]);
$activity->setLeadId($participant->getLeadId());
$hasUpdate = true;
}
if ($hasUpdate) {
$activity->save();
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preference' => 'boolean',
'recording_reason_code' => 'integer',
'has_recording_prompt' => 'boolean',
'on_air' => 'integer',
];
}
protected static function boot()
{
parent::boot();
static::updated(static function (Activity $activity) {
// If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week
if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||
($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {
if ($activity->isDirty('status')) {
event(new StatusUpdated($activity));
}
if ($activity->isDirty('stage_id')) {
event(new StageUpdated($activity));
}
if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {
event(new ProspectUpdated($activity));
}
if ($activity->isDirty('opportunity_id')) {
event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));
}
if ($activity->isDirty('title')) {
event(new TitleUpdated($activity));
}
}
if ($activity->isDirty('playbook_category_id')) {
event(new ActivityTypeUpdated($activity));
}
});
static:...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.12566489,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.37400267,"top":0.10055866,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"9","depth":4,"bounds":{"left":0.38397607,"top":0.10055866,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.39361703,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.40093085,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\Activities\\Crm;\n\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Events\\Activities\\Participants\\Joined;\nuse Jiminny\\Events\\Activities\\Connections\\Opened;\nuse Jiminny\\Exceptions\\HttpSessionExpiredException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Jiminny\\Services\\Crm\\ProspectSearchStrategyFactory;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\n\nclass MatchCrmObject implements ShouldQueue\n{\n /**\n * Match the remote CRM relations to the participant as they join.\n */\n public function handle(Opened|Joined $event): void\n {\n $emailHelper = app(EmailHelper::class);\n $teamRepository = app(TeamRepository::class);\n $logger = app(LoggerInterface::class);\n\n $participant = $event->getParticipant();\n $activity = $event->getActivity();\n\n // Don't bother if they are a team member.\n if ($participant->isTeamMember()) {\n $logger->info('[MatchCrmObject] Participant is team member');\n\n return;\n }\n\n $user = $activity->getUser();\n $team = $user->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $logger->info('[MatchCrmObject] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n $email = $participant->getEmailAddress();\n if ($email !== null\n && ! $emailHelper->shouldPerformLookup($team, $email)\n ) {\n $logger->info('[MatchCrmObject] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $email,\n ]);\n\n $teamMember = $teamRepository->findActiveTeamMemberByEmail($team, $email);\n if ($teamMember instanceof Models\\User) {\n $participant->setUserId($teamMember->getId());\n $participant->save();\n }\n\n return;\n }\n\n // If we have previously matched their crm identity,\n // sync the crm object to get the freshest state, then skip any further lookup\n if ($participant->getLeadId() !== null || $participant->getContactId() !== null) {\n $this->syncActivityCrmFromParticipant($activity, $participant, $logger);\n\n $logger->info('[MatchCrmObject] Participant already has a crm prospect identified, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'email' => $email,\n ]);\n\n return;\n }\n\n try {\n $crmService = $this->getCrmService($team, $user);\n\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $records = null;\n\n if ($email !== null) {\n $records = $decorator->matchExactlyByEmail($email, $user->getId());\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $decorator->matchByPhone($participant->getPhoneNumber(), null, $user->getId());\n }\n\n if (empty($records) && $email !== null) {\n $records = $decorator->matchByDomain($email, $user->getId());\n }\n\n if (! empty($records)) {\n $activity->updateParticipantCrmData($records, $participant);\n $activity->updateActivityCrmData($records);\n }\n } catch (HttpSessionExpiredException|SocialAccountTokenInvalidException $exception) {\n $logger->info('[MatchCrmObject] Activity user triggered exception', [\n 'user_id' => $activity->getUserId(),\n 'activity_id' => $activity->getId(),\n 'exception_message' => $exception->getMessage(),\n 'exception_code' => $exception->getCode(),\n ]);\n }\n }\n\n /**\n * @throws SocialAccountTokenInvalidException\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n */\n private function getCrmService(Models\\Team $team, Models\\User $user): ServiceInterface\n {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n\n return $crmResolver->prepareCrmService();\n }\n\n /**\n * Sync CRM identifiers from a matched participant onto the activity when appropriate.\n */\n private function syncActivityCrmFromParticipant(\n Models\\Activity $activity,\n Models\\Participant $participant,\n LoggerInterface $logger\n ): void {\n $hasUpdate = false;\n\n if ($participant->getContactId() !== null && $activity->getContactId() === null) {\n $logger->info('[MatchCrmObject] Syncing contact_id from participant to activity', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'from_contact_id' => $activity->getContactId(),\n 'to_contact_id' => $participant->getContactId(),\n ]);\n\n $activity->setLeadId(null);\n\n if ($activity->getStage() !== null && $activity->getStage()->getType() === Stage::TYPE_LEAD) {\n $logger->info('[MatchCrmObject] Clearing lead stage on activity due to contact match', [\n 'activity_id' => $activity->getId(),\n 'stage_id' => $activity->getStageId(),\n ]);\n $activity->setStageId(null);\n }\n\n $activity->setContactId($participant->getContactId());\n\n $logger->info('[MatchCrmObject] Activity contact_id set', [\n 'activity_id' => $activity->getId(),\n 'contact_id' => $activity->getContactId(),\n ]);\n\n $hasUpdate = true;\n }\n\n if ($participant->getLeadId() !== null\n && $activity->getLeadId() === null\n && $activity->getContactId() === null\n && $activity->getAccountId() === null\n ) {\n $logger->info('[MatchCrmObject] Syncing lead_id from participant to activity', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'lead_id' => $participant->getLeadId(),\n ]);\n\n $activity->setLeadId($participant->getLeadId());\n $hasUpdate = true;\n }\n\n if ($hasUpdate) {\n $activity->save();\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\Activities\\Crm;\n\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Events\\Activities\\Participants\\Joined;\nuse Jiminny\\Events\\Activities\\Connections\\Opened;\nuse Jiminny\\Exceptions\\HttpSessionExpiredException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Jiminny\\Services\\Crm\\ProspectSearchStrategyFactory;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\n\nclass MatchCrmObject implements ShouldQueue\n{\n /**\n * Match the remote CRM relations to the participant as they join.\n */\n public function handle(Opened|Joined $event): void\n {\n $emailHelper = app(EmailHelper::class);\n $teamRepository = app(TeamRepository::class);\n $logger = app(LoggerInterface::class);\n\n $participant = $event->getParticipant();\n $activity = $event->getActivity();\n\n // Don't bother if they are a team member.\n if ($participant->isTeamMember()) {\n $logger->info('[MatchCrmObject] Participant is team member');\n\n return;\n }\n\n $user = $activity->getUser();\n $team = $user->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $logger->info('[MatchCrmObject] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n $email = $participant->getEmailAddress();\n if ($email !== null\n && ! $emailHelper->shouldPerformLookup($team, $email)\n ) {\n $logger->info('[MatchCrmObject] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $email,\n ]);\n\n $teamMember = $teamRepository->findActiveTeamMemberByEmail($team, $email);\n if ($teamMember instanceof Models\\User) {\n $participant->setUserId($teamMember->getId());\n $participant->save();\n }\n\n return;\n }\n\n // If we have previously matched their crm identity,\n // sync the crm object to get the freshest state, then skip any further lookup\n if ($participant->getLeadId() !== null || $participant->getContactId() !== null) {\n $this->syncActivityCrmFromParticipant($activity, $participant, $logger);\n\n $logger->info('[MatchCrmObject] Participant already has a crm prospect identified, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'email' => $email,\n ]);\n\n return;\n }\n\n try {\n $crmService = $this->getCrmService($team, $user);\n\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $records = null;\n\n if ($email !== null) {\n $records = $decorator->matchExactlyByEmail($email, $user->getId());\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $decorator->matchByPhone($participant->getPhoneNumber(), null, $user->getId());\n }\n\n if (empty($records) && $email !== null) {\n $records = $decorator->matchByDomain($email, $user->getId());\n }\n\n if (! empty($records)) {\n $activity->updateParticipantCrmData($records, $participant);\n $activity->updateActivityCrmData($records);\n }\n } catch (HttpSessionExpiredException|SocialAccountTokenInvalidException $exception) {\n $logger->info('[MatchCrmObject] Activity user triggered exception', [\n 'user_id' => $activity->getUserId(),\n 'activity_id' => $activity->getId(),\n 'exception_message' => $exception->getMessage(),\n 'exception_code' => $exception->getCode(),\n ]);\n }\n }\n\n /**\n * @throws SocialAccountTokenInvalidException\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n */\n private function getCrmService(Models\\Team $team, Models\\User $user): ServiceInterface\n {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n\n return $crmResolver->prepareCrmService();\n }\n\n /**\n * Sync CRM identifiers from a matched participant onto the activity when appropriate.\n */\n private function syncActivityCrmFromParticipant(\n Models\\Activity $activity,\n Models\\Participant $participant,\n LoggerInterface $logger\n ): void {\n $hasUpdate = false;\n\n if ($participant->getContactId() !== null && $activity->getContactId() === null) {\n $logger->info('[MatchCrmObject] Syncing contact_id from participant to activity', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'from_contact_id' => $activity->getContactId(),\n 'to_contact_id' => $participant->getContactId(),\n ]);\n\n $activity->setLeadId(null);\n\n if ($activity->getStage() !== null && $activity->getStage()->getType() === Stage::TYPE_LEAD) {\n $logger->info('[MatchCrmObject] Clearing lead stage on activity due to contact match', [\n 'activity_id' => $activity->getId(),\n 'stage_id' => $activity->getStageId(),\n ]);\n $activity->setStageId(null);\n }\n\n $activity->setContactId($participant->getContactId());\n\n $logger->info('[MatchCrmObject] Activity contact_id set', [\n 'activity_id' => $activity->getId(),\n 'contact_id' => $activity->getContactId(),\n ]);\n\n $hasUpdate = true;\n }\n\n if ($participant->getLeadId() !== null\n && $activity->getLeadId() === null\n && $activity->getContactId() === null\n && $activity->getAccountId() === null\n ) {\n $logger->info('[MatchCrmObject] Syncing lead_id from participant to activity', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'lead_id' => $participant->getLeadId(),\n ]);\n\n $activity->setLeadId($participant->getLeadId());\n $hasUpdate = true;\n }\n\n if ($hasUpdate) {\n $activity->save();\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.6665558,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"169","depth":4,"bounds":{"left":0.6765292,"top":0.12529927,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.69049203,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"102","depth":4,"bounds":{"left":0.70046544,"top":0.12529927,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.7144282,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.7240692,"top":0.123703115,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.73138297,"top":0.123703115,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
7943249111508506828
|
7035618767472895804
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\Activities\Crm;
use Illuminate\Contracts\Queue\ShouldQueue;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Events\Activities\Participants\Joined;
use Jiminny\Events\Activities\Connections\Opened;
use Jiminny\Exceptions\HttpSessionExpiredException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models;
use Jiminny\Models\Stage;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Jiminny\Services\Crm\ProspectSearchStrategyFactory;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
class MatchCrmObject implements ShouldQueue
{
/**
* Match the remote CRM relations to the participant as they join.
*/
public function handle(Opened|Joined $event): void
{
$emailHelper = app(EmailHelper::class);
$teamRepository = app(TeamRepository::class);
$logger = app(LoggerInterface::class);
$participant = $event->getParticipant();
$activity = $event->getActivity();
// Don't bother if they are a team member.
if ($participant->isTeamMember()) {
$logger->info('[MatchCrmObject] Participant is team member');
return;
}
$user = $activity->getUser();
$team = $user->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$logger->info('[MatchCrmObject] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
$email = $participant->getEmailAddress();
if ($email !== null
&& ! $emailHelper->shouldPerformLookup($team, $email)
) {
$logger->info('[MatchCrmObject] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $email,
]);
$teamMember = $teamRepository->findActiveTeamMemberByEmail($team, $email);
if ($teamMember instanceof Models\User) {
$participant->setUserId($teamMember->getId());
$participant->save();
}
return;
}
// If we have previously matched their crm identity,
// sync the crm object to get the freshest state, then skip any further lookup
if ($participant->getLeadId() !== null || $participant->getContactId() !== null) {
$this->syncActivityCrmFromParticipant($activity, $participant, $logger);
$logger->info('[MatchCrmObject] Participant already has a crm prospect identified, skipping crm lookup', [
'activity_id' => $activity->getId(),
'email' => $email,
]);
return;
}
try {
$crmService = $this->getCrmService($team, $user);
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$records = null;
if ($email !== null) {
$records = $decorator->matchExactlyByEmail($email, $user->getId());
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $decorator->matchByPhone($participant->getPhoneNumber(), null, $user->getId());
}
if (empty($records) && $email !== null) {
$records = $decorator->matchByDomain($email, $user->getId());
}
if (! empty($records)) {
$activity->updateParticipantCrmData($records, $participant);
$activity->updateActivityCrmData($records);
}
} catch (HttpSessionExpiredException|SocialAccountTokenInvalidException $exception) {
$logger->info('[MatchCrmObject] Activity user triggered exception', [
'user_id' => $activity->getUserId(),
'activity_id' => $activity->getId(),
'exception_message' => $exception->getMessage(),
'exception_code' => $exception->getCode(),
]);
}
}
/**
* @throws SocialAccountTokenInvalidException
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
private function getCrmService(Models\Team $team, Models\User $user): ServiceInterface
{
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
return $crmResolver->prepareCrmService();
}
/**
* Sync CRM identifiers from a matched participant onto the activity when appropriate.
*/
private function syncActivityCrmFromParticipant(
Models\Activity $activity,
Models\Participant $participant,
LoggerInterface $logger
): void {
$hasUpdate = false;
if ($participant->getContactId() !== null && $activity->getContactId() === null) {
$logger->info('[MatchCrmObject] Syncing contact_id from participant to activity', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'from_contact_id' => $activity->getContactId(),
'to_contact_id' => $participant->getContactId(),
]);
$activity->setLeadId(null);
if ($activity->getStage() !== null && $activity->getStage()->getType() === Stage::TYPE_LEAD) {
$logger->info('[MatchCrmObject] Clearing lead stage on activity due to contact match', [
'activity_id' => $activity->getId(),
'stage_id' => $activity->getStageId(),
]);
$activity->setStageId(null);
}
$activity->setContactId($participant->getContactId());
$logger->info('[MatchCrmObject] Activity contact_id set', [
'activity_id' => $activity->getId(),
'contact_id' => $activity->getContactId(),
]);
$hasUpdate = true;
}
if ($participant->getLeadId() !== null
&& $activity->getLeadId() === null
&& $activity->getContactId() === null
&& $activity->getAccountId() === null
) {
$logger->info('[MatchCrmObject] Syncing lead_id from participant to activity', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'lead_id' => $participant->getLeadId(),
]);
$activity->setLeadId($participant->getLeadId());
$hasUpdate = true;
}
if ($hasUpdate) {
$activity->save();
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preference' => 'boolean',
'recording_reason_code' => 'integer',
'has_recording_prompt' => 'boolean',
'on_air' => 'integer',
];
}
protected static function boot()
{
parent::boot();
static::updated(static function (Activity $activity) {
// If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week
if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||
($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {
if ($activity->isDirty('status')) {
event(new StatusUpdated($activity));
}
if ($activity->isDirty('stage_id')) {
event(new StageUpdated($activity));
}
if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {
event(new ProspectUpdated($activity));
}
if ($activity->isDirty('opportunity_id')) {
event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));
}
if ($activity->isDirty('title')) {
event(new TitleUpdated($activity));
}
}
if ($activity->isDirty('playbook_category_id')) {
event(new ActivityTypeUpdated($activity));
}
});
static:...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
43039
|
1572
|
75
|
2026-05-14T12:15:24.909499+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760924909_m2.jpg...
|
PhpStorm
|
faVsco.js – MatchCrmObject.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\Activities\Crm;
use Illuminate\Contracts\Queue\ShouldQueue;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Events\Activities\Participants\Joined;
use Jiminny\Events\Activities\Connections\Opened;
use Jiminny\Exceptions\HttpSessionExpiredException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models;
use Jiminny\Models\Stage;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Jiminny\Services\Crm\ProspectSearchStrategyFactory;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
class MatchCrmObject implements ShouldQueue
{
/**
* Match the remote CRM relations to the participant as they join.
*/
public function handle(Opened|Joined $event): void
{
$emailHelper = app(EmailHelper::class);
$teamRepository = app(TeamRepository::class);
$logger = app(LoggerInterface::class);
$participant = $event->getParticipant();
$activity = $event->getActivity();
// Don't bother if they are a team member.
if ($participant->isTeamMember()) {
$logger->info('[MatchCrmObject] Participant is team member');
return;
}
$user = $activity->getUser();
$team = $user->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$logger->info('[MatchCrmObject] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
$email = $participant->getEmailAddress();
if ($email !== null
&& ! $emailHelper->shouldPerformLookup($team, $email)
) {
$logger->info('[MatchCrmObject] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $email,
]);
$teamMember = $teamRepository->findActiveTeamMemberByEmail($team, $email);
if ($teamMember instanceof Models\User) {
$participant->setUserId($teamMember->getId());
$participant->save();
}
return;
}
// If we have previously matched their crm identity,
// sync the crm object to get the freshest state, then skip any further lookup
if ($participant->getLeadId() !== null || $participant->getContactId() !== null) {
$this->syncActivityCrmFromParticipant($activity, $participant, $logger);
$logger->info('[MatchCrmObject] Participant already has a crm prospect identified, skipping crm lookup', [
'activity_id' => $activity->getId(),
'email' => $email,
]);
return;
}
try {
$crmService = $this->getCrmService($team, $user);
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$records = null;
if ($email !== null) {
$records = $decorator->matchExactlyByEmail($email, $user->getId());
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $decorator->matchByPhone($participant->getPhoneNumber(), null, $user->getId());
}
if (empty($records) && $email !== null) {
$records = $decorator->matchByDomain($email, $user->getId());
}
if (! empty($records)) {
$activity->updateParticipantCrmData($records, $participant);
$activity->updateActivityCrmData($records);
}
} catch (HttpSessionExpiredException|SocialAccountTokenInvalidException $exception) {
$logger->info('[MatchCrmObject] Activity user triggered exception', [
'user_id' => $activity->getUserId(),
'activity_id' => $activity->getId(),
'exception_message' => $exception->getMessage(),
'exception_code' => $exception->getCode(),
]);
}
}
/**
* @throws SocialAccountTokenInvalidException
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
private function getCrmService(Models\Team $team, Models\User $user): ServiceInterface
{
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
return $crmResolver->prepareCrmService();
}
/**
* Sync CRM identifiers from a matched participant onto the activity when appropriate.
*/
private function syncActivityCrmFromParticipant(
Models\Activity $activity,
Models\Participant $participant,
LoggerInterface $logger
): void {
$hasUpdate = false;
if ($participant->getContactId() !== null && $activity->getContactId() === null) {
$logger->info('[MatchCrmObject] Syncing contact_id from participant to activity', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'from_contact_id' => $activity->getContactId(),
'to_contact_id' => $participant->getContactId(),
]);
$activity->setLeadId(null);
if ($activity->getStage() !== null && $activity->getStage()->getType() === Stage::TYPE_LEAD) {
$logger->info('[MatchCrmObject] Clearing lead stage on activity due to contact match', [
'activity_id' => $activity->getId(),
'stage_id' => $activity->getStageId(),
]);
$activity->setStageId(null);
}
$activity->setContactId($participant->getContactId());
$logger->info('[MatchCrmObject] Activity contact_id set', [
'activity_id' => $activity->getId(),
'contact_id' => $activity->getContactId(),
]);
$hasUpdate = true;
}
if ($participant->getLeadId() !== null
&& $activity->getLeadId() === null
&& $activity->getContactId() === null
&& $activity->getAccountId() === null
) {
$logger->info('[MatchCrmObject] Syncing lead_id from participant to activity', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'lead_id' => $participant->getLeadId(),
]);
$activity->setLeadId($participant->getLeadId());
$hasUpdate = true;
}
if ($hasUpdate) {
$activity->save();
}
}
}...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.12566489,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.37400267,"top":0.10055866,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"9","depth":4,"bounds":{"left":0.38397607,"top":0.10055866,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.39361703,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.40093085,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\Activities\\Crm;\n\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Events\\Activities\\Participants\\Joined;\nuse Jiminny\\Events\\Activities\\Connections\\Opened;\nuse Jiminny\\Exceptions\\HttpSessionExpiredException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Jiminny\\Services\\Crm\\ProspectSearchStrategyFactory;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\n\nclass MatchCrmObject implements ShouldQueue\n{\n /**\n * Match the remote CRM relations to the participant as they join.\n */\n public function handle(Opened|Joined $event): void\n {\n $emailHelper = app(EmailHelper::class);\n $teamRepository = app(TeamRepository::class);\n $logger = app(LoggerInterface::class);\n\n $participant = $event->getParticipant();\n $activity = $event->getActivity();\n\n // Don't bother if they are a team member.\n if ($participant->isTeamMember()) {\n $logger->info('[MatchCrmObject] Participant is team member');\n\n return;\n }\n\n $user = $activity->getUser();\n $team = $user->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $logger->info('[MatchCrmObject] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n $email = $participant->getEmailAddress();\n if ($email !== null\n && ! $emailHelper->shouldPerformLookup($team, $email)\n ) {\n $logger->info('[MatchCrmObject] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $email,\n ]);\n\n $teamMember = $teamRepository->findActiveTeamMemberByEmail($team, $email);\n if ($teamMember instanceof Models\\User) {\n $participant->setUserId($teamMember->getId());\n $participant->save();\n }\n\n return;\n }\n\n // If we have previously matched their crm identity,\n // sync the crm object to get the freshest state, then skip any further lookup\n if ($participant->getLeadId() !== null || $participant->getContactId() !== null) {\n $this->syncActivityCrmFromParticipant($activity, $participant, $logger);\n\n $logger->info('[MatchCrmObject] Participant already has a crm prospect identified, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'email' => $email,\n ]);\n\n return;\n }\n\n try {\n $crmService = $this->getCrmService($team, $user);\n\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $records = null;\n\n if ($email !== null) {\n $records = $decorator->matchExactlyByEmail($email, $user->getId());\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $decorator->matchByPhone($participant->getPhoneNumber(), null, $user->getId());\n }\n\n if (empty($records) && $email !== null) {\n $records = $decorator->matchByDomain($email, $user->getId());\n }\n\n if (! empty($records)) {\n $activity->updateParticipantCrmData($records, $participant);\n $activity->updateActivityCrmData($records);\n }\n } catch (HttpSessionExpiredException|SocialAccountTokenInvalidException $exception) {\n $logger->info('[MatchCrmObject] Activity user triggered exception', [\n 'user_id' => $activity->getUserId(),\n 'activity_id' => $activity->getId(),\n 'exception_message' => $exception->getMessage(),\n 'exception_code' => $exception->getCode(),\n ]);\n }\n }\n\n /**\n * @throws SocialAccountTokenInvalidException\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n */\n private function getCrmService(Models\\Team $team, Models\\User $user): ServiceInterface\n {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n\n return $crmResolver->prepareCrmService();\n }\n\n /**\n * Sync CRM identifiers from a matched participant onto the activity when appropriate.\n */\n private function syncActivityCrmFromParticipant(\n Models\\Activity $activity,\n Models\\Participant $participant,\n LoggerInterface $logger\n ): void {\n $hasUpdate = false;\n\n if ($participant->getContactId() !== null && $activity->getContactId() === null) {\n $logger->info('[MatchCrmObject] Syncing contact_id from participant to activity', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'from_contact_id' => $activity->getContactId(),\n 'to_contact_id' => $participant->getContactId(),\n ]);\n\n $activity->setLeadId(null);\n\n if ($activity->getStage() !== null && $activity->getStage()->getType() === Stage::TYPE_LEAD) {\n $logger->info('[MatchCrmObject] Clearing lead stage on activity due to contact match', [\n 'activity_id' => $activity->getId(),\n 'stage_id' => $activity->getStageId(),\n ]);\n $activity->setStageId(null);\n }\n\n $activity->setContactId($participant->getContactId());\n\n $logger->info('[MatchCrmObject] Activity contact_id set', [\n 'activity_id' => $activity->getId(),\n 'contact_id' => $activity->getContactId(),\n ]);\n\n $hasUpdate = true;\n }\n\n if ($participant->getLeadId() !== null\n && $activity->getLeadId() === null\n && $activity->getContactId() === null\n && $activity->getAccountId() === null\n ) {\n $logger->info('[MatchCrmObject] Syncing lead_id from participant to activity', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'lead_id' => $participant->getLeadId(),\n ]);\n\n $activity->setLeadId($participant->getLeadId());\n $hasUpdate = true;\n }\n\n if ($hasUpdate) {\n $activity->save();\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\Activities\\Crm;\n\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Events\\Activities\\Participants\\Joined;\nuse Jiminny\\Events\\Activities\\Connections\\Opened;\nuse Jiminny\\Exceptions\\HttpSessionExpiredException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Jiminny\\Services\\Crm\\ProspectSearchStrategyFactory;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\n\nclass MatchCrmObject implements ShouldQueue\n{\n /**\n * Match the remote CRM relations to the participant as they join.\n */\n public function handle(Opened|Joined $event): void\n {\n $emailHelper = app(EmailHelper::class);\n $teamRepository = app(TeamRepository::class);\n $logger = app(LoggerInterface::class);\n\n $participant = $event->getParticipant();\n $activity = $event->getActivity();\n\n // Don't bother if they are a team member.\n if ($participant->isTeamMember()) {\n $logger->info('[MatchCrmObject] Participant is team member');\n\n return;\n }\n\n $user = $activity->getUser();\n $team = $user->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $logger->info('[MatchCrmObject] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n $email = $participant->getEmailAddress();\n if ($email !== null\n && ! $emailHelper->shouldPerformLookup($team, $email)\n ) {\n $logger->info('[MatchCrmObject] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $email,\n ]);\n\n $teamMember = $teamRepository->findActiveTeamMemberByEmail($team, $email);\n if ($teamMember instanceof Models\\User) {\n $participant->setUserId($teamMember->getId());\n $participant->save();\n }\n\n return;\n }\n\n // If we have previously matched their crm identity,\n // sync the crm object to get the freshest state, then skip any further lookup\n if ($participant->getLeadId() !== null || $participant->getContactId() !== null) {\n $this->syncActivityCrmFromParticipant($activity, $participant, $logger);\n\n $logger->info('[MatchCrmObject] Participant already has a crm prospect identified, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'email' => $email,\n ]);\n\n return;\n }\n\n try {\n $crmService = $this->getCrmService($team, $user);\n\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $records = null;\n\n if ($email !== null) {\n $records = $decorator->matchExactlyByEmail($email, $user->getId());\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $decorator->matchByPhone($participant->getPhoneNumber(), null, $user->getId());\n }\n\n if (empty($records) && $email !== null) {\n $records = $decorator->matchByDomain($email, $user->getId());\n }\n\n if (! empty($records)) {\n $activity->updateParticipantCrmData($records, $participant);\n $activity->updateActivityCrmData($records);\n }\n } catch (HttpSessionExpiredException|SocialAccountTokenInvalidException $exception) {\n $logger->info('[MatchCrmObject] Activity user triggered exception', [\n 'user_id' => $activity->getUserId(),\n 'activity_id' => $activity->getId(),\n 'exception_message' => $exception->getMessage(),\n 'exception_code' => $exception->getCode(),\n ]);\n }\n }\n\n /**\n * @throws SocialAccountTokenInvalidException\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n */\n private function getCrmService(Models\\Team $team, Models\\User $user): ServiceInterface\n {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n\n return $crmResolver->prepareCrmService();\n }\n\n /**\n * Sync CRM identifiers from a matched participant onto the activity when appropriate.\n */\n private function syncActivityCrmFromParticipant(\n Models\\Activity $activity,\n Models\\Participant $participant,\n LoggerInterface $logger\n ): void {\n $hasUpdate = false;\n\n if ($participant->getContactId() !== null && $activity->getContactId() === null) {\n $logger->info('[MatchCrmObject] Syncing contact_id from participant to activity', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'from_contact_id' => $activity->getContactId(),\n 'to_contact_id' => $participant->getContactId(),\n ]);\n\n $activity->setLeadId(null);\n\n if ($activity->getStage() !== null && $activity->getStage()->getType() === Stage::TYPE_LEAD) {\n $logger->info('[MatchCrmObject] Clearing lead stage on activity due to contact match', [\n 'activity_id' => $activity->getId(),\n 'stage_id' => $activity->getStageId(),\n ]);\n $activity->setStageId(null);\n }\n\n $activity->setContactId($participant->getContactId());\n\n $logger->info('[MatchCrmObject] Activity contact_id set', [\n 'activity_id' => $activity->getId(),\n 'contact_id' => $activity->getContactId(),\n ]);\n\n $hasUpdate = true;\n }\n\n if ($participant->getLeadId() !== null\n && $activity->getLeadId() === null\n && $activity->getContactId() === null\n && $activity->getAccountId() === null\n ) {\n $logger->info('[MatchCrmObject] Syncing lead_id from participant to activity', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'lead_id' => $participant->getLeadId(),\n ]);\n\n $activity->setLeadId($participant->getLeadId());\n $hasUpdate = true;\n }\n\n if ($hasUpdate) {\n $activity->save();\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false}]...
|
-297003181616098622
|
-3896512396625073402
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\Activities\Crm;
use Illuminate\Contracts\Queue\ShouldQueue;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Events\Activities\Participants\Joined;
use Jiminny\Events\Activities\Connections\Opened;
use Jiminny\Exceptions\HttpSessionExpiredException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models;
use Jiminny\Models\Stage;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Jiminny\Services\Crm\ProspectSearchStrategyFactory;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
class MatchCrmObject implements ShouldQueue
{
/**
* Match the remote CRM relations to the participant as they join.
*/
public function handle(Opened|Joined $event): void
{
$emailHelper = app(EmailHelper::class);
$teamRepository = app(TeamRepository::class);
$logger = app(LoggerInterface::class);
$participant = $event->getParticipant();
$activity = $event->getActivity();
// Don't bother if they are a team member.
if ($participant->isTeamMember()) {
$logger->info('[MatchCrmObject] Participant is team member');
return;
}
$user = $activity->getUser();
$team = $user->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$logger->info('[MatchCrmObject] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
$email = $participant->getEmailAddress();
if ($email !== null
&& ! $emailHelper->shouldPerformLookup($team, $email)
) {
$logger->info('[MatchCrmObject] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $email,
]);
$teamMember = $teamRepository->findActiveTeamMemberByEmail($team, $email);
if ($teamMember instanceof Models\User) {
$participant->setUserId($teamMember->getId());
$participant->save();
}
return;
}
// If we have previously matched their crm identity,
// sync the crm object to get the freshest state, then skip any further lookup
if ($participant->getLeadId() !== null || $participant->getContactId() !== null) {
$this->syncActivityCrmFromParticipant($activity, $participant, $logger);
$logger->info('[MatchCrmObject] Participant already has a crm prospect identified, skipping crm lookup', [
'activity_id' => $activity->getId(),
'email' => $email,
]);
return;
}
try {
$crmService = $this->getCrmService($team, $user);
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$records = null;
if ($email !== null) {
$records = $decorator->matchExactlyByEmail($email, $user->getId());
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $decorator->matchByPhone($participant->getPhoneNumber(), null, $user->getId());
}
if (empty($records) && $email !== null) {
$records = $decorator->matchByDomain($email, $user->getId());
}
if (! empty($records)) {
$activity->updateParticipantCrmData($records, $participant);
$activity->updateActivityCrmData($records);
}
} catch (HttpSessionExpiredException|SocialAccountTokenInvalidException $exception) {
$logger->info('[MatchCrmObject] Activity user triggered exception', [
'user_id' => $activity->getUserId(),
'activity_id' => $activity->getId(),
'exception_message' => $exception->getMessage(),
'exception_code' => $exception->getCode(),
]);
}
}
/**
* @throws SocialAccountTokenInvalidException
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
private function getCrmService(Models\Team $team, Models\User $user): ServiceInterface
{
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
return $crmResolver->prepareCrmService();
}
/**
* Sync CRM identifiers from a matched participant onto the activity when appropriate.
*/
private function syncActivityCrmFromParticipant(
Models\Activity $activity,
Models\Participant $participant,
LoggerInterface $logger
): void {
$hasUpdate = false;
if ($participant->getContactId() !== null && $activity->getContactId() === null) {
$logger->info('[MatchCrmObject] Syncing contact_id from participant to activity', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'from_contact_id' => $activity->getContactId(),
'to_contact_id' => $participant->getContactId(),
]);
$activity->setLeadId(null);
if ($activity->getStage() !== null && $activity->getStage()->getType() === Stage::TYPE_LEAD) {
$logger->info('[MatchCrmObject] Clearing lead stage on activity due to contact match', [
'activity_id' => $activity->getId(),
'stage_id' => $activity->getStageId(),
]);
$activity->setStageId(null);
}
$activity->setContactId($participant->getContactId());
$logger->info('[MatchCrmObject] Activity contact_id set', [
'activity_id' => $activity->getId(),
'contact_id' => $activity->getContactId(),
]);
$hasUpdate = true;
}
if ($participant->getLeadId() !== null
&& $activity->getLeadId() === null
&& $activity->getContactId() === null
&& $activity->getAccountId() === null
) {
$logger->info('[MatchCrmObject] Syncing lead_id from participant to activity', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'lead_id' => $participant->getLeadId(),
]);
$activity->setLeadId($participant->getLeadId());
$hasUpdate = true;
}
if ($hasUpdate) {
$activity->save();
}
}
}...
|
43038
|
NULL
|
NULL
|
NULL
|
|
43038
|
1572
|
74
|
2026-05-14T12:15:24.015021+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760924015_m2.jpg...
|
PhpStorm
|
faVsco.js – MatchCrmObject.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\Activities\Crm;
use Illuminate\Contracts\Queue\ShouldQueue;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Events\Activities\Participants\Joined;
use Jiminny\Events\Activities\Connections\Opened;
use Jiminny\Exceptions\HttpSessionExpiredException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models;
use Jiminny\Models\Stage;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Jiminny\Services\Crm\ProspectSearchStrategyFactory;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
class MatchCrmObject implements ShouldQueue
{
/**
* Match the remote CRM relations to the participant as they join.
*/
public function handle(Opened|Joined $event): void
{
$emailHelper = app(EmailHelper::class);
$teamRepository = app(TeamRepository::class);
$logger = app(LoggerInterface::class);
$participant = $event->getParticipant();
$activity = $event->getActivity();
// Don't bother if they are a team member.
if ($participant->isTeamMember()) {
$logger->info('[MatchCrmObject] Participant is team member');
return;
}
$user = $activity->getUser();
$team = $user->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$logger->info('[MatchCrmObject] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
$email = $participant->getEmailAddress();
if ($email !== null
&& ! $emailHelper->shouldPerformLookup($team, $email)
) {
$logger->info('[MatchCrmObject] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $email,
]);
$teamMember = $teamRepository->findActiveTeamMemberByEmail($team, $email);
if ($teamMember instanceof Models\User) {
$participant->setUserId($teamMember->getId());
$participant->save();
}
return;
}
// If we have previously matched their crm identity,
// sync the crm object to get the freshest state, then skip any further lookup
if ($participant->getLeadId() !== null || $participant->getContactId() !== null) {
$this->syncActivityCrmFromParticipant($activity, $participant, $logger);
$logger->info('[MatchCrmObject] Participant already has a crm prospect identified, skipping crm lookup', [
'activity_id' => $activity->getId(),
'email' => $email,
]);
return;
}
try {
$crmService = $this->getCrmService($team, $user);
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$records = null;
if ($email !== null) {
$records = $decorator->matchExactlyByEmail($email, $user->getId());
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $decorator->matchByPhone($participant->getPhoneNumber(), null, $user->getId());
}
if (empty($records) && $email !== null) {
$records = $decorator->matchByDomain($email, $user->getId());
}
if (! empty($records)) {
$activity->updateParticipantCrmData($records, $participant);
$activity->updateActivityCrmData($records);
}
} catch (HttpSessionExpiredException|SocialAccountTokenInvalidException $exception) {
$logger->info('[MatchCrmObject] Activity user triggered exception', [
'user_id' => $activity->getUserId(),
'activity_id' => $activity->getId(),
'exception_message' => $exception->getMessage(),
'exception_code' => $exception->getCode(),
]);
}
}
/**
* @throws SocialAccountTokenInvalidException
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
private function getCrmService(Models\Team $team, Models\User $user): ServiceInterface
{
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
return $crmResolver->prepareCrmService();
}
/**
* Sync CRM identifiers from a matched participant onto the activity when appropriate.
*/
private function syncActivityCrmFromParticipant(
Models\Activity $activity,
Models\Participant $participant,
LoggerInterface $logger
): void {
$hasUpdate = false;
if ($participant->getContactId() !== null && $activity->getContactId() === null) {
$logger->info('[MatchCrmObject] Syncing contact_id from participant to activity', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'from_contact_id' => $activity->getContactId(),
'to_contact_id' => $participant->getContactId(),
]);
$activity->setLeadId(null);
if ($activity->getStage() !== null && $activity->getStage()->getType() === Stage::TYPE_LEAD) {
$logger->info('[MatchCrmObject] Clearing lead stage on activity due to contact match', [
'activity_id' => $activity->getId(),
'stage_id' => $activity->getStageId(),
]);
$activity->setStageId(null);
}
$activity->setContactId($participant->getContactId());
$logger->info('[MatchCrmObject] Activity contact_id set', [
'activity_id' => $activity->getId(),
'contact_id' => $activity->getContactId(),
]);
$hasUpdate = true;
}
if ($participant->getLeadId() !== null
&& $activity->getLeadId() === null
&& $activity->getContactId() === null
&& $activity->getAccountId() === null
) {
$logger->info('[MatchCrmObject] Syncing lead_id from participant to activity', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'lead_id' => $participant->getLeadId(),
]);
$activity->setLeadId($participant->getLeadId());
$hasUpdate = true;
}
if ($hasUpdate) {
$activity->save();
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.12566489,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.37400267,"top":0.10055866,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"9","depth":4,"bounds":{"left":0.38397607,"top":0.10055866,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.39361703,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.40093085,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\Activities\\Crm;\n\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Events\\Activities\\Participants\\Joined;\nuse Jiminny\\Events\\Activities\\Connections\\Opened;\nuse Jiminny\\Exceptions\\HttpSessionExpiredException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Jiminny\\Services\\Crm\\ProspectSearchStrategyFactory;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\n\nclass MatchCrmObject implements ShouldQueue\n{\n /**\n * Match the remote CRM relations to the participant as they join.\n */\n public function handle(Opened|Joined $event): void\n {\n $emailHelper = app(EmailHelper::class);\n $teamRepository = app(TeamRepository::class);\n $logger = app(LoggerInterface::class);\n\n $participant = $event->getParticipant();\n $activity = $event->getActivity();\n\n // Don't bother if they are a team member.\n if ($participant->isTeamMember()) {\n $logger->info('[MatchCrmObject] Participant is team member');\n\n return;\n }\n\n $user = $activity->getUser();\n $team = $user->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $logger->info('[MatchCrmObject] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n $email = $participant->getEmailAddress();\n if ($email !== null\n && ! $emailHelper->shouldPerformLookup($team, $email)\n ) {\n $logger->info('[MatchCrmObject] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $email,\n ]);\n\n $teamMember = $teamRepository->findActiveTeamMemberByEmail($team, $email);\n if ($teamMember instanceof Models\\User) {\n $participant->setUserId($teamMember->getId());\n $participant->save();\n }\n\n return;\n }\n\n // If we have previously matched their crm identity,\n // sync the crm object to get the freshest state, then skip any further lookup\n if ($participant->getLeadId() !== null || $participant->getContactId() !== null) {\n $this->syncActivityCrmFromParticipant($activity, $participant, $logger);\n\n $logger->info('[MatchCrmObject] Participant already has a crm prospect identified, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'email' => $email,\n ]);\n\n return;\n }\n\n try {\n $crmService = $this->getCrmService($team, $user);\n\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $records = null;\n\n if ($email !== null) {\n $records = $decorator->matchExactlyByEmail($email, $user->getId());\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $decorator->matchByPhone($participant->getPhoneNumber(), null, $user->getId());\n }\n\n if (empty($records) && $email !== null) {\n $records = $decorator->matchByDomain($email, $user->getId());\n }\n\n if (! empty($records)) {\n $activity->updateParticipantCrmData($records, $participant);\n $activity->updateActivityCrmData($records);\n }\n } catch (HttpSessionExpiredException|SocialAccountTokenInvalidException $exception) {\n $logger->info('[MatchCrmObject] Activity user triggered exception', [\n 'user_id' => $activity->getUserId(),\n 'activity_id' => $activity->getId(),\n 'exception_message' => $exception->getMessage(),\n 'exception_code' => $exception->getCode(),\n ]);\n }\n }\n\n /**\n * @throws SocialAccountTokenInvalidException\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n */\n private function getCrmService(Models\\Team $team, Models\\User $user): ServiceInterface\n {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n\n return $crmResolver->prepareCrmService();\n }\n\n /**\n * Sync CRM identifiers from a matched participant onto the activity when appropriate.\n */\n private function syncActivityCrmFromParticipant(\n Models\\Activity $activity,\n Models\\Participant $participant,\n LoggerInterface $logger\n ): void {\n $hasUpdate = false;\n\n if ($participant->getContactId() !== null && $activity->getContactId() === null) {\n $logger->info('[MatchCrmObject] Syncing contact_id from participant to activity', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'from_contact_id' => $activity->getContactId(),\n 'to_contact_id' => $participant->getContactId(),\n ]);\n\n $activity->setLeadId(null);\n\n if ($activity->getStage() !== null && $activity->getStage()->getType() === Stage::TYPE_LEAD) {\n $logger->info('[MatchCrmObject] Clearing lead stage on activity due to contact match', [\n 'activity_id' => $activity->getId(),\n 'stage_id' => $activity->getStageId(),\n ]);\n $activity->setStageId(null);\n }\n\n $activity->setContactId($participant->getContactId());\n\n $logger->info('[MatchCrmObject] Activity contact_id set', [\n 'activity_id' => $activity->getId(),\n 'contact_id' => $activity->getContactId(),\n ]);\n\n $hasUpdate = true;\n }\n\n if ($participant->getLeadId() !== null\n && $activity->getLeadId() === null\n && $activity->getContactId() === null\n && $activity->getAccountId() === null\n ) {\n $logger->info('[MatchCrmObject] Syncing lead_id from participant to activity', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'lead_id' => $participant->getLeadId(),\n ]);\n\n $activity->setLeadId($participant->getLeadId());\n $hasUpdate = true;\n }\n\n if ($hasUpdate) {\n $activity->save();\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\Activities\\Crm;\n\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Events\\Activities\\Participants\\Joined;\nuse Jiminny\\Events\\Activities\\Connections\\Opened;\nuse Jiminny\\Exceptions\\HttpSessionExpiredException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Jiminny\\Services\\Crm\\ProspectSearchStrategyFactory;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\n\nclass MatchCrmObject implements ShouldQueue\n{\n /**\n * Match the remote CRM relations to the participant as they join.\n */\n public function handle(Opened|Joined $event): void\n {\n $emailHelper = app(EmailHelper::class);\n $teamRepository = app(TeamRepository::class);\n $logger = app(LoggerInterface::class);\n\n $participant = $event->getParticipant();\n $activity = $event->getActivity();\n\n // Don't bother if they are a team member.\n if ($participant->isTeamMember()) {\n $logger->info('[MatchCrmObject] Participant is team member');\n\n return;\n }\n\n $user = $activity->getUser();\n $team = $user->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $logger->info('[MatchCrmObject] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n $email = $participant->getEmailAddress();\n if ($email !== null\n && ! $emailHelper->shouldPerformLookup($team, $email)\n ) {\n $logger->info('[MatchCrmObject] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $email,\n ]);\n\n $teamMember = $teamRepository->findActiveTeamMemberByEmail($team, $email);\n if ($teamMember instanceof Models\\User) {\n $participant->setUserId($teamMember->getId());\n $participant->save();\n }\n\n return;\n }\n\n // If we have previously matched their crm identity,\n // sync the crm object to get the freshest state, then skip any further lookup\n if ($participant->getLeadId() !== null || $participant->getContactId() !== null) {\n $this->syncActivityCrmFromParticipant($activity, $participant, $logger);\n\n $logger->info('[MatchCrmObject] Participant already has a crm prospect identified, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'email' => $email,\n ]);\n\n return;\n }\n\n try {\n $crmService = $this->getCrmService($team, $user);\n\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $records = null;\n\n if ($email !== null) {\n $records = $decorator->matchExactlyByEmail($email, $user->getId());\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $decorator->matchByPhone($participant->getPhoneNumber(), null, $user->getId());\n }\n\n if (empty($records) && $email !== null) {\n $records = $decorator->matchByDomain($email, $user->getId());\n }\n\n if (! empty($records)) {\n $activity->updateParticipantCrmData($records, $participant);\n $activity->updateActivityCrmData($records);\n }\n } catch (HttpSessionExpiredException|SocialAccountTokenInvalidException $exception) {\n $logger->info('[MatchCrmObject] Activity user triggered exception', [\n 'user_id' => $activity->getUserId(),\n 'activity_id' => $activity->getId(),\n 'exception_message' => $exception->getMessage(),\n 'exception_code' => $exception->getCode(),\n ]);\n }\n }\n\n /**\n * @throws SocialAccountTokenInvalidException\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n */\n private function getCrmService(Models\\Team $team, Models\\User $user): ServiceInterface\n {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n\n return $crmResolver->prepareCrmService();\n }\n\n /**\n * Sync CRM identifiers from a matched participant onto the activity when appropriate.\n */\n private function syncActivityCrmFromParticipant(\n Models\\Activity $activity,\n Models\\Participant $participant,\n LoggerInterface $logger\n ): void {\n $hasUpdate = false;\n\n if ($participant->getContactId() !== null && $activity->getContactId() === null) {\n $logger->info('[MatchCrmObject] Syncing contact_id from participant to activity', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'from_contact_id' => $activity->getContactId(),\n 'to_contact_id' => $participant->getContactId(),\n ]);\n\n $activity->setLeadId(null);\n\n if ($activity->getStage() !== null && $activity->getStage()->getType() === Stage::TYPE_LEAD) {\n $logger->info('[MatchCrmObject] Clearing lead stage on activity due to contact match', [\n 'activity_id' => $activity->getId(),\n 'stage_id' => $activity->getStageId(),\n ]);\n $activity->setStageId(null);\n }\n\n $activity->setContactId($participant->getContactId());\n\n $logger->info('[MatchCrmObject] Activity contact_id set', [\n 'activity_id' => $activity->getId(),\n 'contact_id' => $activity->getContactId(),\n ]);\n\n $hasUpdate = true;\n }\n\n if ($participant->getLeadId() !== null\n && $activity->getLeadId() === null\n && $activity->getContactId() === null\n && $activity->getAccountId() === null\n ) {\n $logger->info('[MatchCrmObject] Syncing lead_id from participant to activity', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'lead_id' => $participant->getLeadId(),\n ]);\n\n $activity->setLeadId($participant->getLeadId());\n $hasUpdate = true;\n }\n\n if ($hasUpdate) {\n $activity->save();\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"4","depth":4,"role_description":"text"}]...
|
467728718167414200
|
-3896512405215007994
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\Activities\Crm;
use Illuminate\Contracts\Queue\ShouldQueue;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Events\Activities\Participants\Joined;
use Jiminny\Events\Activities\Connections\Opened;
use Jiminny\Exceptions\HttpSessionExpiredException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models;
use Jiminny\Models\Stage;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Jiminny\Services\Crm\ProspectSearchStrategyFactory;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
class MatchCrmObject implements ShouldQueue
{
/**
* Match the remote CRM relations to the participant as they join.
*/
public function handle(Opened|Joined $event): void
{
$emailHelper = app(EmailHelper::class);
$teamRepository = app(TeamRepository::class);
$logger = app(LoggerInterface::class);
$participant = $event->getParticipant();
$activity = $event->getActivity();
// Don't bother if they are a team member.
if ($participant->isTeamMember()) {
$logger->info('[MatchCrmObject] Participant is team member');
return;
}
$user = $activity->getUser();
$team = $user->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$logger->info('[MatchCrmObject] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
$email = $participant->getEmailAddress();
if ($email !== null
&& ! $emailHelper->shouldPerformLookup($team, $email)
) {
$logger->info('[MatchCrmObject] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $email,
]);
$teamMember = $teamRepository->findActiveTeamMemberByEmail($team, $email);
if ($teamMember instanceof Models\User) {
$participant->setUserId($teamMember->getId());
$participant->save();
}
return;
}
// If we have previously matched their crm identity,
// sync the crm object to get the freshest state, then skip any further lookup
if ($participant->getLeadId() !== null || $participant->getContactId() !== null) {
$this->syncActivityCrmFromParticipant($activity, $participant, $logger);
$logger->info('[MatchCrmObject] Participant already has a crm prospect identified, skipping crm lookup', [
'activity_id' => $activity->getId(),
'email' => $email,
]);
return;
}
try {
$crmService = $this->getCrmService($team, $user);
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$records = null;
if ($email !== null) {
$records = $decorator->matchExactlyByEmail($email, $user->getId());
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $decorator->matchByPhone($participant->getPhoneNumber(), null, $user->getId());
}
if (empty($records) && $email !== null) {
$records = $decorator->matchByDomain($email, $user->getId());
}
if (! empty($records)) {
$activity->updateParticipantCrmData($records, $participant);
$activity->updateActivityCrmData($records);
}
} catch (HttpSessionExpiredException|SocialAccountTokenInvalidException $exception) {
$logger->info('[MatchCrmObject] Activity user triggered exception', [
'user_id' => $activity->getUserId(),
'activity_id' => $activity->getId(),
'exception_message' => $exception->getMessage(),
'exception_code' => $exception->getCode(),
]);
}
}
/**
* @throws SocialAccountTokenInvalidException
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
private function getCrmService(Models\Team $team, Models\User $user): ServiceInterface
{
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
return $crmResolver->prepareCrmService();
}
/**
* Sync CRM identifiers from a matched participant onto the activity when appropriate.
*/
private function syncActivityCrmFromParticipant(
Models\Activity $activity,
Models\Participant $participant,
LoggerInterface $logger
): void {
$hasUpdate = false;
if ($participant->getContactId() !== null && $activity->getContactId() === null) {
$logger->info('[MatchCrmObject] Syncing contact_id from participant to activity', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'from_contact_id' => $activity->getContactId(),
'to_contact_id' => $participant->getContactId(),
]);
$activity->setLeadId(null);
if ($activity->getStage() !== null && $activity->getStage()->getType() === Stage::TYPE_LEAD) {
$logger->info('[MatchCrmObject] Clearing lead stage on activity due to contact match', [
'activity_id' => $activity->getId(),
'stage_id' => $activity->getStageId(),
]);
$activity->setStageId(null);
}
$activity->setContactId($participant->getContactId());
$logger->info('[MatchCrmObject] Activity contact_id set', [
'activity_id' => $activity->getId(),
'contact_id' => $activity->getContactId(),
]);
$hasUpdate = true;
}
if ($participant->getLeadId() !== null
&& $activity->getLeadId() === null
&& $activity->getContactId() === null
&& $activity->getAccountId() === null
) {
$logger->info('[MatchCrmObject] Syncing lead_id from participant to activity', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'lead_id' => $participant->getLeadId(),
]);
$activity->setLeadId($participant->getLeadId());
$hasUpdate = true;
}
if ($hasUpdate) {
$activity->save();
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
43037
|
1571
|
50
|
2026-05-14T12:15:23.996019+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760923996_m1.jpg...
|
PhpStorm
|
faVsco.js – MatchCrmObject.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\Activities\Crm;
use Illuminate\Contracts\Queue\ShouldQueue;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Events\Activities\Participants\Joined;
use Jiminny\Events\Activities\Connections\Opened;
use Jiminny\Exceptions\HttpSessionExpiredException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models;
use Jiminny\Models\Stage;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Jiminny\Services\Crm\ProspectSearchStrategyFactory;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
class MatchCrmObject implements ShouldQueue
{
/**
* Match the remote CRM relations to the participant as they join.
*/
public function handle(Opened|Joined $event): void
{
$emailHelper = app(EmailHelper::class);
$teamRepository = app(TeamRepository::class);
$logger = app(LoggerInterface::class);
$participant = $event->getParticipant();
$activity = $event->getActivity();
// Don't bother if they are a team member.
if ($participant->isTeamMember()) {
$logger->info('[MatchCrmObject] Participant is team member');
return;
}
$user = $activity->getUser();
$team = $user->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$logger->info('[MatchCrmObject] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
$email = $participant->getEmailAddress();
if ($email !== null
&& ! $emailHelper->shouldPerformLookup($team, $email)
) {
$logger->info('[MatchCrmObject] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $email,
]);
$teamMember = $teamRepository->findActiveTeamMemberByEmail($team, $email);
if ($teamMember instanceof Models\User) {
$participant->setUserId($teamMember->getId());
$participant->save();
}
return;
}
// If we have previously matched their crm identity,
// sync the crm object to get the freshest state, then skip any further lookup
if ($participant->getLeadId() !== null || $participant->getContactId() !== null) {
$this->syncActivityCrmFromParticipant($activity, $participant, $logger);
$logger->info('[MatchCrmObject] Participant already has a crm prospect identified, skipping crm lookup', [
'activity_id' => $activity->getId(),
'email' => $email,
]);
return;
}
try {
$crmService = $this->getCrmService($team, $user);
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$records = null;
if ($email !== null) {
$records = $decorator->matchExactlyByEmail($email, $user->getId());
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $decorator->matchByPhone($participant->getPhoneNumber(), null, $user->getId());
}
if (empty($records) && $email !== null) {
$records = $decorator->matchByDomain($email, $user->getId());
}
if (! empty($records)) {
$activity->updateParticipantCrmData($records, $participant);
$activity->updateActivityCrmData($records);
}
} catch (HttpSessionExpiredException|SocialAccountTokenInvalidException $exception) {
$logger->info('[MatchCrmObject] Activity user triggered exception', [
'user_id' => $activity->getUserId(),
'activity_id' => $activity->getId(),
'exception_message' => $exception->getMessage(),
'exception_code' => $exception->getCode(),
]);
}
}
/**
* @throws SocialAccountTokenInvalidException
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
private function getCrmService(Models\Team $team, Models\User $user): ServiceInterface
{
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
return $crmResolver->prepareCrmService();
}
/**
* Sync CRM identifiers from a matched participant onto the activity when appropriate.
*/
private function syncActivityCrmFromParticipant(
Models\Activity $activity,
Models\Participant $participant,
LoggerInterface $logger
): void {
$hasUpdate = false;
if ($participant->getContactId() !== null && $activity->getContactId() === null) {
$logger->info('[MatchCrmObject] Syncing contact_id from participant to activity', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'from_contact_id' => $activity->getContactId(),
'to_contact_id' => $participant->getContactId(),
]);
$activity->setLeadId(null);
if ($activity->getStage() !== null && $activity->getStage()->getType() === Stage::TYPE_LEAD) {
$logger->info('[MatchCrmObject] Clearing lead stage on activity due to contact match', [
'activity_id' => $activity->getId(),
'stage_id' => $activity->getStageId(),
]);
$activity->setStageId(null);
}
$activity->setContactId($participant->getContactId());
$logger->info('[MatchCrmObject] Activity contact_id set', [
'activity_id' => $activity->getId(),
'contact_id' => $activity->getContactId(),
]);
$hasUpdate = true;
}
if ($participant->getLeadId() !== null
&& $activity->getLeadId() === null
&& $activity->getContactId() === null
&& $activity->getAccountId() === null
) {
$logger->info('[MatchCrmObject] Syncing lead_id from participant to activity', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'lead_id' => $participant->getLeadId(),
]);
$activity->setLeadId($participant->getLeadId());
$hasUpdate = true;
}
if ($hasUpdate) {
$activity->save();
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preference' => 'boolean',
'recording_reason_code' => 'integer',
'has_recording_prompt' => 'boolean',
'on_air' => 'integer',
];
}
protected static function boot()
{
parent::boot();
static::updated(static function (Activity $activity) {
// If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week
if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||
($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {
if ($activity->isDirty('status')) {
event(new StatusUpdated($activity));
}
if ($activity->isDirty('stage_id')) {
event(new StageUpdated($activity));
}
if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {
event(new ProspectUpdated($activity));
}
if ($activity->isDirty('opportunity_id')) {
event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));
}
if ($activity->isDirty('title')) {
event(new TitleUpdated($activity));
}
}
if ($activity->isDirty('playbook_category_id')) {
event(new ActivityTypeUpdated($activity));
}
});
static:...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"9","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\Activities\\Crm;\n\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Events\\Activities\\Participants\\Joined;\nuse Jiminny\\Events\\Activities\\Connections\\Opened;\nuse Jiminny\\Exceptions\\HttpSessionExpiredException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Jiminny\\Services\\Crm\\ProspectSearchStrategyFactory;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\n\nclass MatchCrmObject implements ShouldQueue\n{\n /**\n * Match the remote CRM relations to the participant as they join.\n */\n public function handle(Opened|Joined $event): void\n {\n $emailHelper = app(EmailHelper::class);\n $teamRepository = app(TeamRepository::class);\n $logger = app(LoggerInterface::class);\n\n $participant = $event->getParticipant();\n $activity = $event->getActivity();\n\n // Don't bother if they are a team member.\n if ($participant->isTeamMember()) {\n $logger->info('[MatchCrmObject] Participant is team member');\n\n return;\n }\n\n $user = $activity->getUser();\n $team = $user->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $logger->info('[MatchCrmObject] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n $email = $participant->getEmailAddress();\n if ($email !== null\n && ! $emailHelper->shouldPerformLookup($team, $email)\n ) {\n $logger->info('[MatchCrmObject] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $email,\n ]);\n\n $teamMember = $teamRepository->findActiveTeamMemberByEmail($team, $email);\n if ($teamMember instanceof Models\\User) {\n $participant->setUserId($teamMember->getId());\n $participant->save();\n }\n\n return;\n }\n\n // If we have previously matched their crm identity,\n // sync the crm object to get the freshest state, then skip any further lookup\n if ($participant->getLeadId() !== null || $participant->getContactId() !== null) {\n $this->syncActivityCrmFromParticipant($activity, $participant, $logger);\n\n $logger->info('[MatchCrmObject] Participant already has a crm prospect identified, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'email' => $email,\n ]);\n\n return;\n }\n\n try {\n $crmService = $this->getCrmService($team, $user);\n\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $records = null;\n\n if ($email !== null) {\n $records = $decorator->matchExactlyByEmail($email, $user->getId());\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $decorator->matchByPhone($participant->getPhoneNumber(), null, $user->getId());\n }\n\n if (empty($records) && $email !== null) {\n $records = $decorator->matchByDomain($email, $user->getId());\n }\n\n if (! empty($records)) {\n $activity->updateParticipantCrmData($records, $participant);\n $activity->updateActivityCrmData($records);\n }\n } catch (HttpSessionExpiredException|SocialAccountTokenInvalidException $exception) {\n $logger->info('[MatchCrmObject] Activity user triggered exception', [\n 'user_id' => $activity->getUserId(),\n 'activity_id' => $activity->getId(),\n 'exception_message' => $exception->getMessage(),\n 'exception_code' => $exception->getCode(),\n ]);\n }\n }\n\n /**\n * @throws SocialAccountTokenInvalidException\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n */\n private function getCrmService(Models\\Team $team, Models\\User $user): ServiceInterface\n {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n\n return $crmResolver->prepareCrmService();\n }\n\n /**\n * Sync CRM identifiers from a matched participant onto the activity when appropriate.\n */\n private function syncActivityCrmFromParticipant(\n Models\\Activity $activity,\n Models\\Participant $participant,\n LoggerInterface $logger\n ): void {\n $hasUpdate = false;\n\n if ($participant->getContactId() !== null && $activity->getContactId() === null) {\n $logger->info('[MatchCrmObject] Syncing contact_id from participant to activity', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'from_contact_id' => $activity->getContactId(),\n 'to_contact_id' => $participant->getContactId(),\n ]);\n\n $activity->setLeadId(null);\n\n if ($activity->getStage() !== null && $activity->getStage()->getType() === Stage::TYPE_LEAD) {\n $logger->info('[MatchCrmObject] Clearing lead stage on activity due to contact match', [\n 'activity_id' => $activity->getId(),\n 'stage_id' => $activity->getStageId(),\n ]);\n $activity->setStageId(null);\n }\n\n $activity->setContactId($participant->getContactId());\n\n $logger->info('[MatchCrmObject] Activity contact_id set', [\n 'activity_id' => $activity->getId(),\n 'contact_id' => $activity->getContactId(),\n ]);\n\n $hasUpdate = true;\n }\n\n if ($participant->getLeadId() !== null\n && $activity->getLeadId() === null\n && $activity->getContactId() === null\n && $activity->getAccountId() === null\n ) {\n $logger->info('[MatchCrmObject] Syncing lead_id from participant to activity', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'lead_id' => $participant->getLeadId(),\n ]);\n\n $activity->setLeadId($participant->getLeadId());\n $hasUpdate = true;\n }\n\n if ($hasUpdate) {\n $activity->save();\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\Activities\\Crm;\n\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Events\\Activities\\Participants\\Joined;\nuse Jiminny\\Events\\Activities\\Connections\\Opened;\nuse Jiminny\\Exceptions\\HttpSessionExpiredException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Jiminny\\Services\\Crm\\ProspectSearchStrategyFactory;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\n\nclass MatchCrmObject implements ShouldQueue\n{\n /**\n * Match the remote CRM relations to the participant as they join.\n */\n public function handle(Opened|Joined $event): void\n {\n $emailHelper = app(EmailHelper::class);\n $teamRepository = app(TeamRepository::class);\n $logger = app(LoggerInterface::class);\n\n $participant = $event->getParticipant();\n $activity = $event->getActivity();\n\n // Don't bother if they are a team member.\n if ($participant->isTeamMember()) {\n $logger->info('[MatchCrmObject] Participant is team member');\n\n return;\n }\n\n $user = $activity->getUser();\n $team = $user->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $logger->info('[MatchCrmObject] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n $email = $participant->getEmailAddress();\n if ($email !== null\n && ! $emailHelper->shouldPerformLookup($team, $email)\n ) {\n $logger->info('[MatchCrmObject] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $email,\n ]);\n\n $teamMember = $teamRepository->findActiveTeamMemberByEmail($team, $email);\n if ($teamMember instanceof Models\\User) {\n $participant->setUserId($teamMember->getId());\n $participant->save();\n }\n\n return;\n }\n\n // If we have previously matched their crm identity,\n // sync the crm object to get the freshest state, then skip any further lookup\n if ($participant->getLeadId() !== null || $participant->getContactId() !== null) {\n $this->syncActivityCrmFromParticipant($activity, $participant, $logger);\n\n $logger->info('[MatchCrmObject] Participant already has a crm prospect identified, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'email' => $email,\n ]);\n\n return;\n }\n\n try {\n $crmService = $this->getCrmService($team, $user);\n\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $records = null;\n\n if ($email !== null) {\n $records = $decorator->matchExactlyByEmail($email, $user->getId());\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $decorator->matchByPhone($participant->getPhoneNumber(), null, $user->getId());\n }\n\n if (empty($records) && $email !== null) {\n $records = $decorator->matchByDomain($email, $user->getId());\n }\n\n if (! empty($records)) {\n $activity->updateParticipantCrmData($records, $participant);\n $activity->updateActivityCrmData($records);\n }\n } catch (HttpSessionExpiredException|SocialAccountTokenInvalidException $exception) {\n $logger->info('[MatchCrmObject] Activity user triggered exception', [\n 'user_id' => $activity->getUserId(),\n 'activity_id' => $activity->getId(),\n 'exception_message' => $exception->getMessage(),\n 'exception_code' => $exception->getCode(),\n ]);\n }\n }\n\n /**\n * @throws SocialAccountTokenInvalidException\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n */\n private function getCrmService(Models\\Team $team, Models\\User $user): ServiceInterface\n {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n\n return $crmResolver->prepareCrmService();\n }\n\n /**\n * Sync CRM identifiers from a matched participant onto the activity when appropriate.\n */\n private function syncActivityCrmFromParticipant(\n Models\\Activity $activity,\n Models\\Participant $participant,\n LoggerInterface $logger\n ): void {\n $hasUpdate = false;\n\n if ($participant->getContactId() !== null && $activity->getContactId() === null) {\n $logger->info('[MatchCrmObject] Syncing contact_id from participant to activity', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'from_contact_id' => $activity->getContactId(),\n 'to_contact_id' => $participant->getContactId(),\n ]);\n\n $activity->setLeadId(null);\n\n if ($activity->getStage() !== null && $activity->getStage()->getType() === Stage::TYPE_LEAD) {\n $logger->info('[MatchCrmObject] Clearing lead stage on activity due to contact match', [\n 'activity_id' => $activity->getId(),\n 'stage_id' => $activity->getStageId(),\n ]);\n $activity->setStageId(null);\n }\n\n $activity->setContactId($participant->getContactId());\n\n $logger->info('[MatchCrmObject] Activity contact_id set', [\n 'activity_id' => $activity->getId(),\n 'contact_id' => $activity->getContactId(),\n ]);\n\n $hasUpdate = true;\n }\n\n if ($participant->getLeadId() !== null\n && $activity->getLeadId() === null\n && $activity->getContactId() === null\n && $activity->getAccountId() === null\n ) {\n $logger->info('[MatchCrmObject] Syncing lead_id from participant to activity', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'lead_id' => $participant->getLeadId(),\n ]);\n\n $activity->setLeadId($participant->getLeadId());\n $hasUpdate = true;\n }\n\n if ($hasUpdate) {\n $activity->save();\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"169","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"102","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
7943249111508506828
|
7035618767472895804
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\Activities\Crm;
use Illuminate\Contracts\Queue\ShouldQueue;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Events\Activities\Participants\Joined;
use Jiminny\Events\Activities\Connections\Opened;
use Jiminny\Exceptions\HttpSessionExpiredException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models;
use Jiminny\Models\Stage;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Jiminny\Services\Crm\ProspectSearchStrategyFactory;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
class MatchCrmObject implements ShouldQueue
{
/**
* Match the remote CRM relations to the participant as they join.
*/
public function handle(Opened|Joined $event): void
{
$emailHelper = app(EmailHelper::class);
$teamRepository = app(TeamRepository::class);
$logger = app(LoggerInterface::class);
$participant = $event->getParticipant();
$activity = $event->getActivity();
// Don't bother if they are a team member.
if ($participant->isTeamMember()) {
$logger->info('[MatchCrmObject] Participant is team member');
return;
}
$user = $activity->getUser();
$team = $user->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$logger->info('[MatchCrmObject] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
$email = $participant->getEmailAddress();
if ($email !== null
&& ! $emailHelper->shouldPerformLookup($team, $email)
) {
$logger->info('[MatchCrmObject] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $email,
]);
$teamMember = $teamRepository->findActiveTeamMemberByEmail($team, $email);
if ($teamMember instanceof Models\User) {
$participant->setUserId($teamMember->getId());
$participant->save();
}
return;
}
// If we have previously matched their crm identity,
// sync the crm object to get the freshest state, then skip any further lookup
if ($participant->getLeadId() !== null || $participant->getContactId() !== null) {
$this->syncActivityCrmFromParticipant($activity, $participant, $logger);
$logger->info('[MatchCrmObject] Participant already has a crm prospect identified, skipping crm lookup', [
'activity_id' => $activity->getId(),
'email' => $email,
]);
return;
}
try {
$crmService = $this->getCrmService($team, $user);
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$records = null;
if ($email !== null) {
$records = $decorator->matchExactlyByEmail($email, $user->getId());
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $decorator->matchByPhone($participant->getPhoneNumber(), null, $user->getId());
}
if (empty($records) && $email !== null) {
$records = $decorator->matchByDomain($email, $user->getId());
}
if (! empty($records)) {
$activity->updateParticipantCrmData($records, $participant);
$activity->updateActivityCrmData($records);
}
} catch (HttpSessionExpiredException|SocialAccountTokenInvalidException $exception) {
$logger->info('[MatchCrmObject] Activity user triggered exception', [
'user_id' => $activity->getUserId(),
'activity_id' => $activity->getId(),
'exception_message' => $exception->getMessage(),
'exception_code' => $exception->getCode(),
]);
}
}
/**
* @throws SocialAccountTokenInvalidException
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
private function getCrmService(Models\Team $team, Models\User $user): ServiceInterface
{
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
return $crmResolver->prepareCrmService();
}
/**
* Sync CRM identifiers from a matched participant onto the activity when appropriate.
*/
private function syncActivityCrmFromParticipant(
Models\Activity $activity,
Models\Participant $participant,
LoggerInterface $logger
): void {
$hasUpdate = false;
if ($participant->getContactId() !== null && $activity->getContactId() === null) {
$logger->info('[MatchCrmObject] Syncing contact_id from participant to activity', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'from_contact_id' => $activity->getContactId(),
'to_contact_id' => $participant->getContactId(),
]);
$activity->setLeadId(null);
if ($activity->getStage() !== null && $activity->getStage()->getType() === Stage::TYPE_LEAD) {
$logger->info('[MatchCrmObject] Clearing lead stage on activity due to contact match', [
'activity_id' => $activity->getId(),
'stage_id' => $activity->getStageId(),
]);
$activity->setStageId(null);
}
$activity->setContactId($participant->getContactId());
$logger->info('[MatchCrmObject] Activity contact_id set', [
'activity_id' => $activity->getId(),
'contact_id' => $activity->getContactId(),
]);
$hasUpdate = true;
}
if ($participant->getLeadId() !== null
&& $activity->getLeadId() === null
&& $activity->getContactId() === null
&& $activity->getAccountId() === null
) {
$logger->info('[MatchCrmObject] Syncing lead_id from participant to activity', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'lead_id' => $participant->getLeadId(),
]);
$activity->setLeadId($participant->getLeadId());
$hasUpdate = true;
}
if ($hasUpdate) {
$activity->save();
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preference' => 'boolean',
'recording_reason_code' => 'integer',
'has_recording_prompt' => 'boolean',
'on_air' => 'integer',
];
}
protected static function boot()
{
parent::boot();
static::updated(static function (Activity $activity) {
// If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week
if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||
($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {
if ($activity->isDirty('status')) {
event(new StatusUpdated($activity));
}
if ($activity->isDirty('stage_id')) {
event(new StageUpdated($activity));
}
if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {
event(new ProspectUpdated($activity));
}
if ($activity->isDirty('opportunity_id')) {
event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));
}
if ($activity->isDirty('title')) {
event(new TitleUpdated($activity));
}
}
if ($activity->isDirty('playbook_category_id')) {
event(new ActivityTypeUpdated($activity));
}
});
static:...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
43036
|
1572
|
73
|
2026-05-14T12:15:21.253268+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760921253_m2.jpg...
|
PhpStorm
|
faVsco.js – MatchCrmObject.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\Activities\Crm;
use Illuminate\Contracts\Queue\ShouldQueue;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Events\Activities\Participants\Joined;
use Jiminny\Events\Activities\Connections\Opened;
use Jiminny\Exceptions\HttpSessionExpiredException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models;
use Jiminny\Models\Stage;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Jiminny\Services\Crm\ProspectSearchStrategyFactory;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
class MatchCrmObject implements ShouldQueue
{
/**
* Match the remote CRM relations to the participant as they join.
*/
public function handle(Opened|Joined $event): void
{
$emailHelper = app(EmailHelper::class);
$teamRepository = app(TeamRepository::class);
$logger = app(LoggerInterface::class);
$participant = $event->getParticipant();
$activity = $event->getActivity();
// Don't bother if they are a team member.
if ($participant->isTeamMember()) {
$logger->info('[MatchCrmObject] Participant is team member');
return;
}
$user = $activity->getUser();
$team = $user->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$logger->info('[MatchCrmObject] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
$email = $participant->getEmailAddress();
if ($email !== null
&& ! $emailHelper->shouldPerformLookup($team, $email)
) {
$logger->info('[MatchCrmObject] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $email,
]);
$teamMember = $teamRepository->findActiveTeamMemberByEmail($team, $email);
if ($teamMember instanceof Models\User) {
$participant->setUserId($teamMember->getId());
$participant->save();
}
return;
}
// If we have previously matched their crm identity,
// sync the crm object to get the freshest state, then skip any further lookup
if ($participant->getLeadId() !== null || $participant->getContactId() !== null) {
$this->syncActivityCrmFromParticipant($activity, $participant, $logger);
$logger->info('[MatchCrmObject] Participant already has a crm prospect identified, skipping crm lookup', [
'activity_id' => $activity->getId(),
'email' => $email,
]);
return;
}
try {
$crmService = $this->getCrmService($team, $user);
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$records = null;
if ($email !== null) {
$records = $decorator->matchExactlyByEmail($email, $user->getId());
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $decorator->matchByPhone($participant->getPhoneNumber(), null, $user->getId());
}
if (empty($records) && $email !== null) {
$records = $decorator->matchByDomain($email, $user->getId());
}
if (! empty($records)) {
$activity->updateParticipantCrmData($records, $participant);
$activity->updateActivityCrmData($records);
}
} catch (HttpSessionExpiredException|SocialAccountTokenInvalidException $exception) {
$logger->info('[MatchCrmObject] Activity user triggered exception', [
'user_id' => $activity->getUserId(),
'activity_id' => $activity->getId(),
'exception_message' => $exception->getMessage(),
'exception_code' => $exception->getCode(),
]);
}
}
/**
* @throws SocialAccountTokenInvalidException
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
private function getCrmService(Models\Team $team, Models\User $user): ServiceInterface
{
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
return $crmResolver->prepareCrmService();
}
/**
* Sync CRM identifiers from a matched participant onto the activity when appropriate.
*/
private function syncActivityCrmFromParticipant(
Models\Activity $activity,
Models\Participant $participant,
LoggerInterface $logger
): void {
$hasUpdate = false;
if ($participant->getContactId() !== null && $activity->getContactId() === null) {
$logger->info('[MatchCrmObject] Syncing contact_id from participant to activity', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'from_contact_id' => $activity->getContactId(),
'to_contact_id' => $participant->getContactId(),
]);
$activity->setLeadId(null);
if ($activity->getStage() !== null && $activity->getStage()->getType() === Stage::TYPE_LEAD) {
$logger->info('[MatchCrmObject] Clearing lead stage on activity due to contact match', [
'activity_id' => $activity->getId(),
'stage_id' => $activity->getStageId(),
]);
$activity->setStageId(null);
}
$activity->setContactId($participant->getContactId());
$logger->info('[MatchCrmObject] Activity contact_id set', [
'activity_id' => $activity->getId(),
'contact_id' => $activity->getContactId(),
]);
$hasUpdate = true;
}
if ($participant->getLeadId() !== null
&& $activity->getLeadId() === null
&& $activity->getContactId() === null
&& $activity->getAccountId() === null
) {
$logger->info('[MatchCrmObject] Syncing lead_id from participant to activity', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'lead_id' => $participant->getLeadId(),
]);
$activity->setLeadId($participant->getLeadId());
$hasUpdate = true;
}
if ($hasUpdate) {
$activity->save();
}
}
}...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.12566489,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.37400267,"top":0.10055866,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"9","depth":4,"bounds":{"left":0.38397607,"top":0.10055866,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.39361703,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.40093085,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\Activities\\Crm;\n\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Events\\Activities\\Participants\\Joined;\nuse Jiminny\\Events\\Activities\\Connections\\Opened;\nuse Jiminny\\Exceptions\\HttpSessionExpiredException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Jiminny\\Services\\Crm\\ProspectSearchStrategyFactory;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\n\nclass MatchCrmObject implements ShouldQueue\n{\n /**\n * Match the remote CRM relations to the participant as they join.\n */\n public function handle(Opened|Joined $event): void\n {\n $emailHelper = app(EmailHelper::class);\n $teamRepository = app(TeamRepository::class);\n $logger = app(LoggerInterface::class);\n\n $participant = $event->getParticipant();\n $activity = $event->getActivity();\n\n // Don't bother if they are a team member.\n if ($participant->isTeamMember()) {\n $logger->info('[MatchCrmObject] Participant is team member');\n\n return;\n }\n\n $user = $activity->getUser();\n $team = $user->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $logger->info('[MatchCrmObject] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n $email = $participant->getEmailAddress();\n if ($email !== null\n && ! $emailHelper->shouldPerformLookup($team, $email)\n ) {\n $logger->info('[MatchCrmObject] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $email,\n ]);\n\n $teamMember = $teamRepository->findActiveTeamMemberByEmail($team, $email);\n if ($teamMember instanceof Models\\User) {\n $participant->setUserId($teamMember->getId());\n $participant->save();\n }\n\n return;\n }\n\n // If we have previously matched their crm identity,\n // sync the crm object to get the freshest state, then skip any further lookup\n if ($participant->getLeadId() !== null || $participant->getContactId() !== null) {\n $this->syncActivityCrmFromParticipant($activity, $participant, $logger);\n\n $logger->info('[MatchCrmObject] Participant already has a crm prospect identified, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'email' => $email,\n ]);\n\n return;\n }\n\n try {\n $crmService = $this->getCrmService($team, $user);\n\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $records = null;\n\n if ($email !== null) {\n $records = $decorator->matchExactlyByEmail($email, $user->getId());\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $decorator->matchByPhone($participant->getPhoneNumber(), null, $user->getId());\n }\n\n if (empty($records) && $email !== null) {\n $records = $decorator->matchByDomain($email, $user->getId());\n }\n\n if (! empty($records)) {\n $activity->updateParticipantCrmData($records, $participant);\n $activity->updateActivityCrmData($records);\n }\n } catch (HttpSessionExpiredException|SocialAccountTokenInvalidException $exception) {\n $logger->info('[MatchCrmObject] Activity user triggered exception', [\n 'user_id' => $activity->getUserId(),\n 'activity_id' => $activity->getId(),\n 'exception_message' => $exception->getMessage(),\n 'exception_code' => $exception->getCode(),\n ]);\n }\n }\n\n /**\n * @throws SocialAccountTokenInvalidException\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n */\n private function getCrmService(Models\\Team $team, Models\\User $user): ServiceInterface\n {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n\n return $crmResolver->prepareCrmService();\n }\n\n /**\n * Sync CRM identifiers from a matched participant onto the activity when appropriate.\n */\n private function syncActivityCrmFromParticipant(\n Models\\Activity $activity,\n Models\\Participant $participant,\n LoggerInterface $logger\n ): void {\n $hasUpdate = false;\n\n if ($participant->getContactId() !== null && $activity->getContactId() === null) {\n $logger->info('[MatchCrmObject] Syncing contact_id from participant to activity', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'from_contact_id' => $activity->getContactId(),\n 'to_contact_id' => $participant->getContactId(),\n ]);\n\n $activity->setLeadId(null);\n\n if ($activity->getStage() !== null && $activity->getStage()->getType() === Stage::TYPE_LEAD) {\n $logger->info('[MatchCrmObject] Clearing lead stage on activity due to contact match', [\n 'activity_id' => $activity->getId(),\n 'stage_id' => $activity->getStageId(),\n ]);\n $activity->setStageId(null);\n }\n\n $activity->setContactId($participant->getContactId());\n\n $logger->info('[MatchCrmObject] Activity contact_id set', [\n 'activity_id' => $activity->getId(),\n 'contact_id' => $activity->getContactId(),\n ]);\n\n $hasUpdate = true;\n }\n\n if ($participant->getLeadId() !== null\n && $activity->getLeadId() === null\n && $activity->getContactId() === null\n && $activity->getAccountId() === null\n ) {\n $logger->info('[MatchCrmObject] Syncing lead_id from participant to activity', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'lead_id' => $participant->getLeadId(),\n ]);\n\n $activity->setLeadId($participant->getLeadId());\n $hasUpdate = true;\n }\n\n if ($hasUpdate) {\n $activity->save();\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Listeners\\Activities\\Crm;\n\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Events\\Activities\\Participants\\Joined;\nuse Jiminny\\Events\\Activities\\Connections\\Opened;\nuse Jiminny\\Exceptions\\HttpSessionExpiredException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Jiminny\\Services\\Crm\\ProspectSearchStrategyFactory;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\n\nclass MatchCrmObject implements ShouldQueue\n{\n /**\n * Match the remote CRM relations to the participant as they join.\n */\n public function handle(Opened|Joined $event): void\n {\n $emailHelper = app(EmailHelper::class);\n $teamRepository = app(TeamRepository::class);\n $logger = app(LoggerInterface::class);\n\n $participant = $event->getParticipant();\n $activity = $event->getActivity();\n\n // Don't bother if they are a team member.\n if ($participant->isTeamMember()) {\n $logger->info('[MatchCrmObject] Participant is team member');\n\n return;\n }\n\n $user = $activity->getUser();\n $team = $user->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $logger->info('[MatchCrmObject] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n $email = $participant->getEmailAddress();\n if ($email !== null\n && ! $emailHelper->shouldPerformLookup($team, $email)\n ) {\n $logger->info('[MatchCrmObject] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $email,\n ]);\n\n $teamMember = $teamRepository->findActiveTeamMemberByEmail($team, $email);\n if ($teamMember instanceof Models\\User) {\n $participant->setUserId($teamMember->getId());\n $participant->save();\n }\n\n return;\n }\n\n // If we have previously matched their crm identity,\n // sync the crm object to get the freshest state, then skip any further lookup\n if ($participant->getLeadId() !== null || $participant->getContactId() !== null) {\n $this->syncActivityCrmFromParticipant($activity, $participant, $logger);\n\n $logger->info('[MatchCrmObject] Participant already has a crm prospect identified, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'email' => $email,\n ]);\n\n return;\n }\n\n try {\n $crmService = $this->getCrmService($team, $user);\n\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $records = null;\n\n if ($email !== null) {\n $records = $decorator->matchExactlyByEmail($email, $user->getId());\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $decorator->matchByPhone($participant->getPhoneNumber(), null, $user->getId());\n }\n\n if (empty($records) && $email !== null) {\n $records = $decorator->matchByDomain($email, $user->getId());\n }\n\n if (! empty($records)) {\n $activity->updateParticipantCrmData($records, $participant);\n $activity->updateActivityCrmData($records);\n }\n } catch (HttpSessionExpiredException|SocialAccountTokenInvalidException $exception) {\n $logger->info('[MatchCrmObject] Activity user triggered exception', [\n 'user_id' => $activity->getUserId(),\n 'activity_id' => $activity->getId(),\n 'exception_message' => $exception->getMessage(),\n 'exception_code' => $exception->getCode(),\n ]);\n }\n }\n\n /**\n * @throws SocialAccountTokenInvalidException\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n */\n private function getCrmService(Models\\Team $team, Models\\User $user): ServiceInterface\n {\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n\n return $crmResolver->prepareCrmService();\n }\n\n /**\n * Sync CRM identifiers from a matched participant onto the activity when appropriate.\n */\n private function syncActivityCrmFromParticipant(\n Models\\Activity $activity,\n Models\\Participant $participant,\n LoggerInterface $logger\n ): void {\n $hasUpdate = false;\n\n if ($participant->getContactId() !== null && $activity->getContactId() === null) {\n $logger->info('[MatchCrmObject] Syncing contact_id from participant to activity', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'from_contact_id' => $activity->getContactId(),\n 'to_contact_id' => $participant->getContactId(),\n ]);\n\n $activity->setLeadId(null);\n\n if ($activity->getStage() !== null && $activity->getStage()->getType() === Stage::TYPE_LEAD) {\n $logger->info('[MatchCrmObject] Clearing lead stage on activity due to contact match', [\n 'activity_id' => $activity->getId(),\n 'stage_id' => $activity->getStageId(),\n ]);\n $activity->setStageId(null);\n }\n\n $activity->setContactId($participant->getContactId());\n\n $logger->info('[MatchCrmObject] Activity contact_id set', [\n 'activity_id' => $activity->getId(),\n 'contact_id' => $activity->getContactId(),\n ]);\n\n $hasUpdate = true;\n }\n\n if ($participant->getLeadId() !== null\n && $activity->getLeadId() === null\n && $activity->getContactId() === null\n && $activity->getAccountId() === null\n ) {\n $logger->info('[MatchCrmObject] Syncing lead_id from participant to activity', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'lead_id' => $participant->getLeadId(),\n ]);\n\n $activity->setLeadId($participant->getLeadId());\n $hasUpdate = true;\n }\n\n if ($hasUpdate) {\n $activity->save();\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false}]...
|
-297003181616098622
|
-3896512396625073402
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
2
9
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Listeners\Activities\Crm;
use Illuminate\Contracts\Queue\ShouldQueue;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Events\Activities\Participants\Joined;
use Jiminny\Events\Activities\Connections\Opened;
use Jiminny\Exceptions\HttpSessionExpiredException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models;
use Jiminny\Models\Stage;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Jiminny\Services\Crm\ProspectSearchStrategyFactory;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
class MatchCrmObject implements ShouldQueue
{
/**
* Match the remote CRM relations to the participant as they join.
*/
public function handle(Opened|Joined $event): void
{
$emailHelper = app(EmailHelper::class);
$teamRepository = app(TeamRepository::class);
$logger = app(LoggerInterface::class);
$participant = $event->getParticipant();
$activity = $event->getActivity();
// Don't bother if they are a team member.
if ($participant->isTeamMember()) {
$logger->info('[MatchCrmObject] Participant is team member');
return;
}
$user = $activity->getUser();
$team = $user->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$logger->info('[MatchCrmObject] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
$email = $participant->getEmailAddress();
if ($email !== null
&& ! $emailHelper->shouldPerformLookup($team, $email)
) {
$logger->info('[MatchCrmObject] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $email,
]);
$teamMember = $teamRepository->findActiveTeamMemberByEmail($team, $email);
if ($teamMember instanceof Models\User) {
$participant->setUserId($teamMember->getId());
$participant->save();
}
return;
}
// If we have previously matched their crm identity,
// sync the crm object to get the freshest state, then skip any further lookup
if ($participant->getLeadId() !== null || $participant->getContactId() !== null) {
$this->syncActivityCrmFromParticipant($activity, $participant, $logger);
$logger->info('[MatchCrmObject] Participant already has a crm prospect identified, skipping crm lookup', [
'activity_id' => $activity->getId(),
'email' => $email,
]);
return;
}
try {
$crmService = $this->getCrmService($team, $user);
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$records = null;
if ($email !== null) {
$records = $decorator->matchExactlyByEmail($email, $user->getId());
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $decorator->matchByPhone($participant->getPhoneNumber(), null, $user->getId());
}
if (empty($records) && $email !== null) {
$records = $decorator->matchByDomain($email, $user->getId());
}
if (! empty($records)) {
$activity->updateParticipantCrmData($records, $participant);
$activity->updateActivityCrmData($records);
}
} catch (HttpSessionExpiredException|SocialAccountTokenInvalidException $exception) {
$logger->info('[MatchCrmObject] Activity user triggered exception', [
'user_id' => $activity->getUserId(),
'activity_id' => $activity->getId(),
'exception_message' => $exception->getMessage(),
'exception_code' => $exception->getCode(),
]);
}
}
/**
* @throws SocialAccountTokenInvalidException
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
private function getCrmService(Models\Team $team, Models\User $user): ServiceInterface
{
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
return $crmResolver->prepareCrmService();
}
/**
* Sync CRM identifiers from a matched participant onto the activity when appropriate.
*/
private function syncActivityCrmFromParticipant(
Models\Activity $activity,
Models\Participant $participant,
LoggerInterface $logger
): void {
$hasUpdate = false;
if ($participant->getContactId() !== null && $activity->getContactId() === null) {
$logger->info('[MatchCrmObject] Syncing contact_id from participant to activity', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'from_contact_id' => $activity->getContactId(),
'to_contact_id' => $participant->getContactId(),
]);
$activity->setLeadId(null);
if ($activity->getStage() !== null && $activity->getStage()->getType() === Stage::TYPE_LEAD) {
$logger->info('[MatchCrmObject] Clearing lead stage on activity due to contact match', [
'activity_id' => $activity->getId(),
'stage_id' => $activity->getStageId(),
]);
$activity->setStageId(null);
}
$activity->setContactId($participant->getContactId());
$logger->info('[MatchCrmObject] Activity contact_id set', [
'activity_id' => $activity->getId(),
'contact_id' => $activity->getContactId(),
]);
$hasUpdate = true;
}
if ($participant->getLeadId() !== null
&& $activity->getLeadId() === null
&& $activity->getContactId() === null
&& $activity->getAccountId() === null
) {
$logger->info('[MatchCrmObject] Syncing lead_id from participant to activity', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'lead_id' => $participant->getLeadId(),
]);
$activity->setLeadId($participant->getLeadId());
$hasUpdate = true;
}
if ($hasUpdate) {
$activity->save();
}
}
}...
|
43035
|
NULL
|
NULL
|
NULL
|
|
43035
|
1572
|
72
|
2026-05-14T12:15:18.639540+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760918639_m2.jpg...
|
PhpStorm
|
faVsco.js – UpdateCrmData.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
10
3
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Listeners\Activities\Crm;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Collection;
use Jiminny\Contracts\ActivityAwareInterface;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Activity\AssignOwnership;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\BaseService;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Jiminny\Services\Crm\ProspectSearchStrategyFactory;
use Jiminny\Services\Crm\RecordSelector;
use Psr\Log\LoggerInterface;
class UpdateCrmData implements ShouldQueue
{
private LoggerInterface $logger;
private EmailHelper $emailHelper;
private TeamRepository $teamRepository;
private ?Team $team = null;
public function __construct()
{
$this->logger = app(LoggerInterface::class);
$this->emailHelper = app(EmailHelper::class);
$this->teamRepository = app(TeamRepository::class);
}
/**
* Attempt to automatically update crm data if needed.
*
* @param $event \Jiminny\Events\Activities\Conferences\Started
*/
public function handle(ActivityAwareInterface $event): void
{
$activity = $event->getActivity();
$participants = $activity->getParticipants();
try {
$user = $activity->getUser();
$this->team = $user->getTeam();
$crmService = $this->getCrmService($this->team, $user);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return;
}
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($this->team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
// Unset any associated crm Data
$this->logger->info('[UpdateCrmData] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
} else {
$records = $this->updateParticipantsCrmData($crmService, $activity, $participants);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
}
// Now we know who the meeting is with, try and re-assign ownership if necessary.
dispatch(new AssignOwnership($activity->getUuid()));
}
/**
* @param Collection<Participant> $participants
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
ServiceInterface $crmService,
Activity $activity,
Collection $participants
): array {
$emailRecords = [];
$domainRecords = [];
foreach ($participants as $participant) {
if ($participant->hasUser() || $participant->email === null) {
continue;
}
if (! $this->emailHelper->shouldPerformLookup($this->team, $participant->email)) {
$this->logger->info('[UpdateCrmData] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $this->team->getId(),
'email' => $participant->email,
]);
// Update participant with jiminny account
$user = $this->teamRepository->findActiveTeamMemberByEmail($this->team, $participant->email);
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
continue;
}
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$records = $decorator->matchExactlyByEmail($participant->email, $activity->getUser()->getId());
if (! empty($records)) {
$emailRecords[] = $records;
}
if (empty($records)) {
$records = $decorator->matchByDomain($participant->email, $activity->getUser()->getId());
if (! empty($records)) {
$domainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
continue;
}
}
return RecordSelector::pickBestFromLists($emailRecords, $domainRecords);
}
/**
* @param Team $team
* @param User $user
*
* @throws SocialAccountTokenInvalidException
* @throws \Psr\Container\ContainerExceptionInterface
* @throws \Psr\Container\NotFoundExceptionInterface
*
* @return ServiceInterface|BaseService
*/
private function getCrmService(Team $team, User $user): BaseService|ServiceInterface
{
/** @var CrmOwnerResolver $crmResolver */
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
return $crmResolver->prepareCrmService();
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preference' => 'boolean',
'recording_reason_code' => 'integer',
'has_recording_prompt' => 'boolean',
'on_air' => 'integer',
];
}
protected static function boot()
{
parent::boot();
static::updated(static function (Activity $activity) {
// If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week
if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||
($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {
if ($activity->isDirty('status')) {
event(new StatusUpdated($activity));
}
if ($activity->isDirty('stage_id')) {
event(new StageUpdated($activity));
}
if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {
event(new ProspectUpdated($activity));
}
if ($activity->isDirty('opportunity_id')) {
event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));
}
if ($activity->isDirty('title')) {
event(new TitleUpdated($activity));
}
}
if ($activity->isDirty('playbook_category_id')) {
event(new ActivityTypeUpdated($activity));
}
});
static::deleted(static function (Activity $activity) {
// Hard delete associated playlistActivities
$activity->playlistActivities()->delete();
});
}
public function getOrganizerAttribute(): ?Participant
{
$participant = $this->participants()->where('user_id', $this->user_id)->first();
if (! $participant instanceof Participant && $participant !== null) {
throw new RuntimeException(sprintf('$participant must be an instance of "%s" or null', Participant::class));
}
return $participant;
}
public function getFormattedValueAttribute()
{
$currencyCode = 'USD';
if ($this->opportunity) {
$currencyCode = $this->opportunity->getCurrencyCode();
}
$formatter = new CurrencyFormatter();
$formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);
$formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);
return $formatter->format($this->value, $currencyCode);
}
public function getProspectNameAttribute(): ?string
{
$prospectName = null;
if ($this->lead_id) {
$prospectName = $this->lead->name;
} elseif ($this->contact_id) {
$prospectName = $this->contact->name;
} elseif ($this->account_id) {
...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.12566489,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10","depth":4,"bounds":{"left":0.3723404,"top":0.10055866,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.38397607,"top":0.10055866,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.39361703,"top":0.09896249,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.40093085,"top":0.09896249,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Listeners\\Activities\\Crm;\n\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\ActivityAwareInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Activity\\AssignOwnership;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\BaseService;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Jiminny\\Services\\Crm\\ProspectSearchStrategyFactory;\nuse Jiminny\\Services\\Crm\\RecordSelector;\nuse Psr\\Log\\LoggerInterface;\n\nclass UpdateCrmData implements ShouldQueue\n{\n private LoggerInterface $logger;\n private EmailHelper $emailHelper;\n private TeamRepository $teamRepository;\n private ?Team $team = null;\n\n public function __construct()\n {\n $this->logger = app(LoggerInterface::class);\n $this->emailHelper = app(EmailHelper::class);\n $this->teamRepository = app(TeamRepository::class);\n }\n\n /**\n * Attempt to automatically update crm data if needed.\n *\n * @param $event \\Jiminny\\Events\\Activities\\Conferences\\Started\n */\n public function handle(ActivityAwareInterface $event): void\n {\n $activity = $event->getActivity();\n $participants = $activity->getParticipants();\n\n try {\n $user = $activity->getUser();\n $this->team = $user->getTeam();\n\n $crmService = $this->getCrmService($this->team, $user);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return;\n }\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($this->team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n // Unset any associated crm Data\n $this->logger->info('[UpdateCrmData] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n } else {\n $records = $this->updateParticipantsCrmData($crmService, $activity, $participants);\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n }\n\n // Now we know who the meeting is with, try and re-assign ownership if necessary.\n dispatch(new AssignOwnership($activity->getUuid()));\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n ServiceInterface $crmService,\n Activity $activity,\n Collection $participants\n ): array {\n $emailRecords = [];\n $domainRecords = [];\n\n foreach ($participants as $participant) {\n if ($participant->hasUser() || $participant->email === null) {\n continue;\n }\n\n if (! $this->emailHelper->shouldPerformLookup($this->team, $participant->email)) {\n $this->logger->info('[UpdateCrmData] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $this->team->getId(),\n 'email' => $participant->email,\n ]);\n\n // Update participant with jiminny account\n $user = $this->teamRepository->findActiveTeamMemberByEmail($this->team, $participant->email);\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n\n continue;\n }\n\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $records = $decorator->matchExactlyByEmail($participant->email, $activity->getUser()->getId());\n\n if (! empty($records)) {\n $emailRecords[] = $records;\n }\n\n if (empty($records)) {\n $records = $decorator->matchByDomain($participant->email, $activity->getUser()->getId());\n if (! empty($records)) {\n $domainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n continue;\n }\n }\n\n return RecordSelector::pickBestFromLists($emailRecords, $domainRecords);\n }\n\n /**\n * @param Team $team\n * @param User $user\n *\n * @throws SocialAccountTokenInvalidException\n * @throws \\Psr\\Container\\ContainerExceptionInterface\n * @throws \\Psr\\Container\\NotFoundExceptionInterface\n *\n * @return ServiceInterface|BaseService\n */\n private function getCrmService(Team $team, User $user): BaseService|ServiceInterface\n {\n /** @var CrmOwnerResolver $crmResolver */\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n\n return $crmResolver->prepareCrmService();\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Listeners\\Activities\\Crm;\n\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\ActivityAwareInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Activity\\AssignOwnership;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\BaseService;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Jiminny\\Services\\Crm\\ProspectSearchStrategyFactory;\nuse Jiminny\\Services\\Crm\\RecordSelector;\nuse Psr\\Log\\LoggerInterface;\n\nclass UpdateCrmData implements ShouldQueue\n{\n private LoggerInterface $logger;\n private EmailHelper $emailHelper;\n private TeamRepository $teamRepository;\n private ?Team $team = null;\n\n public function __construct()\n {\n $this->logger = app(LoggerInterface::class);\n $this->emailHelper = app(EmailHelper::class);\n $this->teamRepository = app(TeamRepository::class);\n }\n\n /**\n * Attempt to automatically update crm data if needed.\n *\n * @param $event \\Jiminny\\Events\\Activities\\Conferences\\Started\n */\n public function handle(ActivityAwareInterface $event): void\n {\n $activity = $event->getActivity();\n $participants = $activity->getParticipants();\n\n try {\n $user = $activity->getUser();\n $this->team = $user->getTeam();\n\n $crmService = $this->getCrmService($this->team, $user);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return;\n }\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($this->team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n // Unset any associated crm Data\n $this->logger->info('[UpdateCrmData] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n } else {\n $records = $this->updateParticipantsCrmData($crmService, $activity, $participants);\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n }\n\n // Now we know who the meeting is with, try and re-assign ownership if necessary.\n dispatch(new AssignOwnership($activity->getUuid()));\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n ServiceInterface $crmService,\n Activity $activity,\n Collection $participants\n ): array {\n $emailRecords = [];\n $domainRecords = [];\n\n foreach ($participants as $participant) {\n if ($participant->hasUser() || $participant->email === null) {\n continue;\n }\n\n if (! $this->emailHelper->shouldPerformLookup($this->team, $participant->email)) {\n $this->logger->info('[UpdateCrmData] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $this->team->getId(),\n 'email' => $participant->email,\n ]);\n\n // Update participant with jiminny account\n $user = $this->teamRepository->findActiveTeamMemberByEmail($this->team, $participant->email);\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n\n continue;\n }\n\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $records = $decorator->matchExactlyByEmail($participant->email, $activity->getUser()->getId());\n\n if (! empty($records)) {\n $emailRecords[] = $records;\n }\n\n if (empty($records)) {\n $records = $decorator->matchByDomain($participant->email, $activity->getUser()->getId());\n if (! empty($records)) {\n $domainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n continue;\n }\n }\n\n return RecordSelector::pickBestFromLists($emailRecords, $domainRecords);\n }\n\n /**\n * @param Team $team\n * @param User $user\n *\n * @throws SocialAccountTokenInvalidException\n * @throws \\Psr\\Container\\ContainerExceptionInterface\n * @throws \\Psr\\Container\\NotFoundExceptionInterface\n *\n * @return ServiceInterface|BaseService\n */\n private function getCrmService(Team $team, User $user): BaseService|ServiceInterface\n {\n /** @var CrmOwnerResolver $crmResolver */\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n\n return $crmResolver->prepareCrmService();\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.6665558,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"169","depth":4,"bounds":{"left":0.6765292,"top":0.12529927,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.69049203,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"102","depth":4,"bounds":{"left":0.70046544,"top":0.12529927,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.7144282,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.7240692,"top":0.123703115,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.73138297,"top":0.123703115,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"app ~/jiminny/app","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".circleci","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".cursor","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".github","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".sonarlint","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".vscode","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".windsurf","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"app, sources root","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Actions","depth":8,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"InviteUserToTeamAction.php, class","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MarkUserAsOnboardableAction.php, class","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncRecordingFlagsAction.php, class","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"UpdateTeamMemberAction.php, class","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"UpdateUserRolesAction.php, class","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Component","depth":8,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Acl","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActionItems","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Activity","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivityAnalytics","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivitySearch","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"EventSubscriber","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FilterDefinition","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DealInsights","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Security","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"TeamInsights","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivityActualDate.php, class","depth":11,"on_screen":false,"role_description":"text"}]...
|
-1954779490219343768
|
6999589850261956412
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
10
3
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Listeners\Activities\Crm;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Collection;
use Jiminny\Contracts\ActivityAwareInterface;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Activity\AssignOwnership;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\BaseService;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Jiminny\Services\Crm\ProspectSearchStrategyFactory;
use Jiminny\Services\Crm\RecordSelector;
use Psr\Log\LoggerInterface;
class UpdateCrmData implements ShouldQueue
{
private LoggerInterface $logger;
private EmailHelper $emailHelper;
private TeamRepository $teamRepository;
private ?Team $team = null;
public function __construct()
{
$this->logger = app(LoggerInterface::class);
$this->emailHelper = app(EmailHelper::class);
$this->teamRepository = app(TeamRepository::class);
}
/**
* Attempt to automatically update crm data if needed.
*
* @param $event \Jiminny\Events\Activities\Conferences\Started
*/
public function handle(ActivityAwareInterface $event): void
{
$activity = $event->getActivity();
$participants = $activity->getParticipants();
try {
$user = $activity->getUser();
$this->team = $user->getTeam();
$crmService = $this->getCrmService($this->team, $user);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return;
}
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($this->team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
// Unset any associated crm Data
$this->logger->info('[UpdateCrmData] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
} else {
$records = $this->updateParticipantsCrmData($crmService, $activity, $participants);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
}
// Now we know who the meeting is with, try and re-assign ownership if necessary.
dispatch(new AssignOwnership($activity->getUuid()));
}
/**
* @param Collection<Participant> $participants
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
ServiceInterface $crmService,
Activity $activity,
Collection $participants
): array {
$emailRecords = [];
$domainRecords = [];
foreach ($participants as $participant) {
if ($participant->hasUser() || $participant->email === null) {
continue;
}
if (! $this->emailHelper->shouldPerformLookup($this->team, $participant->email)) {
$this->logger->info('[UpdateCrmData] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $this->team->getId(),
'email' => $participant->email,
]);
// Update participant with jiminny account
$user = $this->teamRepository->findActiveTeamMemberByEmail($this->team, $participant->email);
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
continue;
}
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$records = $decorator->matchExactlyByEmail($participant->email, $activity->getUser()->getId());
if (! empty($records)) {
$emailRecords[] = $records;
}
if (empty($records)) {
$records = $decorator->matchByDomain($participant->email, $activity->getUser()->getId());
if (! empty($records)) {
$domainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
continue;
}
}
return RecordSelector::pickBestFromLists($emailRecords, $domainRecords);
}
/**
* @param Team $team
* @param User $user
*
* @throws SocialAccountTokenInvalidException
* @throws \Psr\Container\ContainerExceptionInterface
* @throws \Psr\Container\NotFoundExceptionInterface
*
* @return ServiceInterface|BaseService
*/
private function getCrmService(Team $team, User $user): BaseService|ServiceInterface
{
/** @var CrmOwnerResolver $crmResolver */
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
return $crmResolver->prepareCrmService();
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preference' => 'boolean',
'recording_reason_code' => 'integer',
'has_recording_prompt' => 'boolean',
'on_air' => 'integer',
];
}
protected static function boot()
{
parent::boot();
static::updated(static function (Activity $activity) {
// If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week
if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||
($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {
if ($activity->isDirty('status')) {
event(new StatusUpdated($activity));
}
if ($activity->isDirty('stage_id')) {
event(new StageUpdated($activity));
}
if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {
event(new ProspectUpdated($activity));
}
if ($activity->isDirty('opportunity_id')) {
event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));
}
if ($activity->isDirty('title')) {
event(new TitleUpdated($activity));
}
}
if ($activity->isDirty('playbook_category_id')) {
event(new ActivityTypeUpdated($activity));
}
});
static::deleted(static function (Activity $activity) {
// Hard delete associated playlistActivities
$activity->playlistActivities()->delete();
});
}
public function getOrganizerAttribute(): ?Participant
{
$participant = $this->participants()->where('user_id', $this->user_id)->first();
if (! $participant instanceof Participant && $participant !== null) {
throw new RuntimeException(sprintf('$participant must be an instance of "%s" or null', Participant::class));
}
return $participant;
}
public function getFormattedValueAttribute()
{
$currencyCode = 'USD';
if ($this->opportunity) {
$currencyCode = $this->opportunity->getCurrencyCode();
}
$formatter = new CurrencyFormatter();
$formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);
$formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);
return $formatter->format($this->value, $currencyCode);
}
public function getProspectNameAttribute(): ?string
{
$prospectName = null;
if ($this->lead_id) {
$prospectName = $this->lead->name;
} elseif ($this->contact_id) {
$prospectName = $this->contact->name;
} elseif ($this->account_id) {
...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
43034
|
1571
|
49
|
2026-05-14T12:15:18.627315+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760918627_m1.jpg...
|
PhpStorm
|
faVsco.js – UpdateCrmData.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
10
3
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Listeners\Activities\Crm;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Collection;
use Jiminny\Contracts\ActivityAwareInterface;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Activity\AssignOwnership;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\BaseService;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Jiminny\Services\Crm\ProspectSearchStrategyFactory;
use Jiminny\Services\Crm\RecordSelector;
use Psr\Log\LoggerInterface;
class UpdateCrmData implements ShouldQueue
{
private LoggerInterface $logger;
private EmailHelper $emailHelper;
private TeamRepository $teamRepository;
private ?Team $team = null;
public function __construct()
{
$this->logger = app(LoggerInterface::class);
$this->emailHelper = app(EmailHelper::class);
$this->teamRepository = app(TeamRepository::class);
}
/**
* Attempt to automatically update crm data if needed.
*
* @param $event \Jiminny\Events\Activities\Conferences\Started
*/
public function handle(ActivityAwareInterface $event): void
{
$activity = $event->getActivity();
$participants = $activity->getParticipants();
try {
$user = $activity->getUser();
$this->team = $user->getTeam();
$crmService = $this->getCrmService($this->team, $user);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return;
}
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($this->team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
// Unset any associated crm Data
$this->logger->info('[UpdateCrmData] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
} else {
$records = $this->updateParticipantsCrmData($crmService, $activity, $participants);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
}
// Now we know who the meeting is with, try and re-assign ownership if necessary.
dispatch(new AssignOwnership($activity->getUuid()));
}
/**
* @param Collection<Participant> $participants
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
ServiceInterface $crmService,
Activity $activity,
Collection $participants
): array {
$emailRecords = [];
$domainRecords = [];
foreach ($participants as $participant) {
if ($participant->hasUser() || $participant->email === null) {
continue;
}
if (! $this->emailHelper->shouldPerformLookup($this->team, $participant->email)) {
$this->logger->info('[UpdateCrmData] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $this->team->getId(),
'email' => $participant->email,
]);
// Update participant with jiminny account
$user = $this->teamRepository->findActiveTeamMemberByEmail($this->team, $participant->email);
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
continue;
}
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$records = $decorator->matchExactlyByEmail($participant->email, $activity->getUser()->getId());
if (! empty($records)) {
$emailRecords[] = $records;
}
if (empty($records)) {
$records = $decorator->matchByDomain($participant->email, $activity->getUser()->getId());
if (! empty($records)) {
$domainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
continue;
}
}
return RecordSelector::pickBestFromLists($emailRecords, $domainRecords);
}
/**
* @param Team $team
* @param User $user
*
* @throws SocialAccountTokenInvalidException
* @throws \Psr\Container\ContainerExceptionInterface
* @throws \Psr\Container\NotFoundExceptionInterface
*
* @return ServiceInterface|BaseService
*/
private function getCrmService(Team $team, User $user): BaseService|ServiceInterface
{
/** @var CrmOwnerResolver $crmResolver */
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
return $crmResolver->prepareCrmService();
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preference' => 'boolean',
'recording_reason_code' => 'integer',
'has_recording_prompt' => 'boolean',
'on_air' => 'integer',
];
}
protected static function boot()
{
parent::boot();
static::updated(static function (Activity $activity) {
// If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week
if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||
($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {
if ($activity->isDirty('status')) {
event(new StatusUpdated($activity));
}
if ($activity->isDirty('stage_id')) {
event(new StageUpdated($activity));
}
if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {
event(new ProspectUpdated($activity));
}
if ($activity->isDirty('opportunity_id')) {
event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));
}
if ($activity->isDirty('title')) {
event(new TitleUpdated($activity));
}
}
if ($activity->isDirty('playbook_category_id')) {
event(new ActivityTypeUpdated($activity));
}
});
static::deleted(static function (Activity $activity) {
// Hard delete associated playlistActivities
$activity->playlistActivities()->delete();
});
}
public function getOrganizerAttribute(): ?Participant
{
$participant = $this->participants()->where('user_id', $this->user_id)->first();
if (! $participant instanceof Participant && $participant !== null) {
throw new RuntimeException(sprintf('$participant must be an instance of "%s" or null', Participant::class));
}
return $participant;
}
public function getFormattedValueAttribute()
{
$currencyCode = 'USD';
if ($this->opportunity) {
$currencyCode = $this->opportunity->getCurrencyCode();
}
$formatter = new CurrencyFormatter();
$formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);
$formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);
return $formatter->format($this->value, $currencyCode);
}
public function getProspectNameAttribute(): ?string
{
$prospectName = null;
if ($this->lead_id) {
$prospectName = $this->lead->name;
} elseif ($this->contact_id) {
$prospectName = $this->contact->name;
} elseif ($this->account_id) {
...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"10","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Listeners\\Activities\\Crm;\n\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\ActivityAwareInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Activity\\AssignOwnership;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\BaseService;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Jiminny\\Services\\Crm\\ProspectSearchStrategyFactory;\nuse Jiminny\\Services\\Crm\\RecordSelector;\nuse Psr\\Log\\LoggerInterface;\n\nclass UpdateCrmData implements ShouldQueue\n{\n private LoggerInterface $logger;\n private EmailHelper $emailHelper;\n private TeamRepository $teamRepository;\n private ?Team $team = null;\n\n public function __construct()\n {\n $this->logger = app(LoggerInterface::class);\n $this->emailHelper = app(EmailHelper::class);\n $this->teamRepository = app(TeamRepository::class);\n }\n\n /**\n * Attempt to automatically update crm data if needed.\n *\n * @param $event \\Jiminny\\Events\\Activities\\Conferences\\Started\n */\n public function handle(ActivityAwareInterface $event): void\n {\n $activity = $event->getActivity();\n $participants = $activity->getParticipants();\n\n try {\n $user = $activity->getUser();\n $this->team = $user->getTeam();\n\n $crmService = $this->getCrmService($this->team, $user);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return;\n }\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($this->team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n // Unset any associated crm Data\n $this->logger->info('[UpdateCrmData] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n } else {\n $records = $this->updateParticipantsCrmData($crmService, $activity, $participants);\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n }\n\n // Now we know who the meeting is with, try and re-assign ownership if necessary.\n dispatch(new AssignOwnership($activity->getUuid()));\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n ServiceInterface $crmService,\n Activity $activity,\n Collection $participants\n ): array {\n $emailRecords = [];\n $domainRecords = [];\n\n foreach ($participants as $participant) {\n if ($participant->hasUser() || $participant->email === null) {\n continue;\n }\n\n if (! $this->emailHelper->shouldPerformLookup($this->team, $participant->email)) {\n $this->logger->info('[UpdateCrmData] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $this->team->getId(),\n 'email' => $participant->email,\n ]);\n\n // Update participant with jiminny account\n $user = $this->teamRepository->findActiveTeamMemberByEmail($this->team, $participant->email);\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n\n continue;\n }\n\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $records = $decorator->matchExactlyByEmail($participant->email, $activity->getUser()->getId());\n\n if (! empty($records)) {\n $emailRecords[] = $records;\n }\n\n if (empty($records)) {\n $records = $decorator->matchByDomain($participant->email, $activity->getUser()->getId());\n if (! empty($records)) {\n $domainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n continue;\n }\n }\n\n return RecordSelector::pickBestFromLists($emailRecords, $domainRecords);\n }\n\n /**\n * @param Team $team\n * @param User $user\n *\n * @throws SocialAccountTokenInvalidException\n * @throws \\Psr\\Container\\ContainerExceptionInterface\n * @throws \\Psr\\Container\\NotFoundExceptionInterface\n *\n * @return ServiceInterface|BaseService\n */\n private function getCrmService(Team $team, User $user): BaseService|ServiceInterface\n {\n /** @var CrmOwnerResolver $crmResolver */\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n\n return $crmResolver->prepareCrmService();\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Listeners\\Activities\\Crm;\n\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\ActivityAwareInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Activity\\AssignOwnership;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\TeamRepository;\nuse Jiminny\\Services\\Activity\\CrmOwnerResolver;\nuse Jiminny\\Services\\Crm\\BaseService;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Jiminny\\Services\\Crm\\ProspectSearchStrategyFactory;\nuse Jiminny\\Services\\Crm\\RecordSelector;\nuse Psr\\Log\\LoggerInterface;\n\nclass UpdateCrmData implements ShouldQueue\n{\n private LoggerInterface $logger;\n private EmailHelper $emailHelper;\n private TeamRepository $teamRepository;\n private ?Team $team = null;\n\n public function __construct()\n {\n $this->logger = app(LoggerInterface::class);\n $this->emailHelper = app(EmailHelper::class);\n $this->teamRepository = app(TeamRepository::class);\n }\n\n /**\n * Attempt to automatically update crm data if needed.\n *\n * @param $event \\Jiminny\\Events\\Activities\\Conferences\\Started\n */\n public function handle(ActivityAwareInterface $event): void\n {\n $activity = $event->getActivity();\n $participants = $activity->getParticipants();\n\n try {\n $user = $activity->getUser();\n $this->team = $user->getTeam();\n\n $crmService = $this->getCrmService($this->team, $user);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n return;\n }\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($this->team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n // Unset any associated crm Data\n $this->logger->info('[UpdateCrmData] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n } else {\n $records = $this->updateParticipantsCrmData($crmService, $activity, $participants);\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n }\n\n // Now we know who the meeting is with, try and re-assign ownership if necessary.\n dispatch(new AssignOwnership($activity->getUuid()));\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n ServiceInterface $crmService,\n Activity $activity,\n Collection $participants\n ): array {\n $emailRecords = [];\n $domainRecords = [];\n\n foreach ($participants as $participant) {\n if ($participant->hasUser() || $participant->email === null) {\n continue;\n }\n\n if (! $this->emailHelper->shouldPerformLookup($this->team, $participant->email)) {\n $this->logger->info('[UpdateCrmData] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $this->team->getId(),\n 'email' => $participant->email,\n ]);\n\n // Update participant with jiminny account\n $user = $this->teamRepository->findActiveTeamMemberByEmail($this->team, $participant->email);\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n\n continue;\n }\n\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($crmService);\n\n $records = $decorator->matchExactlyByEmail($participant->email, $activity->getUser()->getId());\n\n if (! empty($records)) {\n $emailRecords[] = $records;\n }\n\n if (empty($records)) {\n $records = $decorator->matchByDomain($participant->email, $activity->getUser()->getId());\n if (! empty($records)) {\n $domainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (\\Exception $ex) {\n \\Sentry::captureException($ex);\n\n continue;\n }\n }\n\n return RecordSelector::pickBestFromLists($emailRecords, $domainRecords);\n }\n\n /**\n * @param Team $team\n * @param User $user\n *\n * @throws SocialAccountTokenInvalidException\n * @throws \\Psr\\Container\\ContainerExceptionInterface\n * @throws \\Psr\\Container\\NotFoundExceptionInterface\n *\n * @return ServiceInterface|BaseService\n */\n private function getCrmService(Team $team, User $user): BaseService|ServiceInterface\n {\n /** @var CrmOwnerResolver $crmResolver */\n $crmResolver = app(CrmOwnerResolver::class, [\n 'team' => $team,\n 'integrationAdmin' => $user,\n 'providerSlug' => $team->getCrmConfiguration()->getProviderName(),\n ]);\n\n return $crmResolver->prepareCrmService();\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"169","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"102","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"app ~/jiminny/app","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".circleci","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".cursor","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".github","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".sonarlint","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".vscode","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".windsurf","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"app, sources root","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Actions","depth":8,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"InviteUserToTeamAction.php, class","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MarkUserAsOnboardableAction.php, class","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncRecordingFlagsAction.php, class","depth":9,"on_screen":false,"role_description":"text"}]...
|
-1954779490219343768
|
6999589850261956412
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
10
3
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Listeners\Activities\Crm;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Collection;
use Jiminny\Contracts\ActivityAwareInterface;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Activity\AssignOwnership;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Repositories\TeamRepository;
use Jiminny\Services\Activity\CrmOwnerResolver;
use Jiminny\Services\Crm\BaseService;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Jiminny\Services\Crm\ProspectSearchStrategyFactory;
use Jiminny\Services\Crm\RecordSelector;
use Psr\Log\LoggerInterface;
class UpdateCrmData implements ShouldQueue
{
private LoggerInterface $logger;
private EmailHelper $emailHelper;
private TeamRepository $teamRepository;
private ?Team $team = null;
public function __construct()
{
$this->logger = app(LoggerInterface::class);
$this->emailHelper = app(EmailHelper::class);
$this->teamRepository = app(TeamRepository::class);
}
/**
* Attempt to automatically update crm data if needed.
*
* @param $event \Jiminny\Events\Activities\Conferences\Started
*/
public function handle(ActivityAwareInterface $event): void
{
$activity = $event->getActivity();
$participants = $activity->getParticipants();
try {
$user = $activity->getUser();
$this->team = $user->getTeam();
$crmService = $this->getCrmService($this->team, $user);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
return;
}
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($this->team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
// Unset any associated crm Data
$this->logger->info('[UpdateCrmData] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
} else {
$records = $this->updateParticipantsCrmData($crmService, $activity, $participants);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
}
// Now we know who the meeting is with, try and re-assign ownership if necessary.
dispatch(new AssignOwnership($activity->getUuid()));
}
/**
* @param Collection<Participant> $participants
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
ServiceInterface $crmService,
Activity $activity,
Collection $participants
): array {
$emailRecords = [];
$domainRecords = [];
foreach ($participants as $participant) {
if ($participant->hasUser() || $participant->email === null) {
continue;
}
if (! $this->emailHelper->shouldPerformLookup($this->team, $participant->email)) {
$this->logger->info('[UpdateCrmData] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $this->team->getId(),
'email' => $participant->email,
]);
// Update participant with jiminny account
$user = $this->teamRepository->findActiveTeamMemberByEmail($this->team, $participant->email);
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
continue;
}
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($crmService);
$records = $decorator->matchExactlyByEmail($participant->email, $activity->getUser()->getId());
if (! empty($records)) {
$emailRecords[] = $records;
}
if (empty($records)) {
$records = $decorator->matchByDomain($participant->email, $activity->getUser()->getId());
if (! empty($records)) {
$domainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (\Exception $ex) {
\Sentry::captureException($ex);
continue;
}
}
return RecordSelector::pickBestFromLists($emailRecords, $domainRecords);
}
/**
* @param Team $team
* @param User $user
*
* @throws SocialAccountTokenInvalidException
* @throws \Psr\Container\ContainerExceptionInterface
* @throws \Psr\Container\NotFoundExceptionInterface
*
* @return ServiceInterface|BaseService
*/
private function getCrmService(Team $team, User $user): BaseService|ServiceInterface
{
/** @var CrmOwnerResolver $crmResolver */
$crmResolver = app(CrmOwnerResolver::class, [
'team' => $team,
'integrationAdmin' => $user,
'providerSlug' => $team->getCrmConfiguration()->getProviderName(),
]);
return $crmResolver->prepareCrmService();
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
self::STATUS_SENT,
self::STATUS_RESENT,
self::STATUS_DELIVERED,
self::STATUS_UNDELIVERED,
self::STATUS_RECEIVING,
self::STATUS_RECEIVED,
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_STARTING_SOON,
self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,
self::STATUS_BOT_INSTANCE_STARTED,
self::STATUS_DUPLICATED,
];
public static array $enumProviders = [
self::PROVIDER_TWILIO,
self::PROVIDER_OUTREACH,
self::PROVIDER_ZOOM_BOT,
self::PROVIDER_SALESLOFT,
self::PROVIDER_AIRCALL,
self::PROVIDER_JUSTCALL,
self::PROVIDER_GOOGLE_MEET,
self::PROVIDER_GONG,
self::PROVIDER_HUBSPOT,
self::PROVIDER_CLOSE,
self::PROVIDER_TEAMS,
self::PROVIDER_SALESFORCE,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_GOOGLE,
self::PROVIDER_OFFICE,
self::PROVIDER_NATTERBOX,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_RINGCENTRAL_VIDEO,
self::PROVIDER_GOTOMEETING,
self::PROVIDER_DEMODESK,
self::PROVIDER_DIALPAD,
self::PROVIDER_ZOOM_PHONE,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
];
public static $enumRecordingStates = [
self::RECORDING_OFF, // Default state
self::RECORDING_IN_PROGRESS,
self::RECORDING_PAUSED,
self::RECORDING_STOPPED,
self::RECORDING_RECORDED,
self::RECORDING_FAILED,
];
// @Important:
// This collection is not used anywhere, and is fully duplicated by the Channels const.
// Validate if it is referred somehow via the enum trait, and if not, remove it entirely.
// An even better strategy will be to move all those constants to a dedicated class
protected array $enumTypes = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
protected static $enumFailedStatuses = [
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_BUSY,
self::STATUS_CANCELLED,
];
protected $table = 'activities';
protected $fillable = [
// Type of activity.
'type', // @todo refactor to `channel`
// The activity type.
'playbook_category_id',
// User who hosts the activity.
'user_id',
// Related Lead record (if applicable)
'lead_id',
// Related Account record (if applicable)
'account_id',
// Related Contact record (if applicable)
'contact_id',
// Related Opportunity record (if applicable)
'opportunity_id',
// Stage of activity.
'stage_id',
// Value of opportunity.
'value',
// If the activity relates to a CRM task.
'crm_provider_id',
// If the activity was created through an external device.
'device_id',
// the activity's language code
'language',
// transcription id
'transcription_id',
// Duration of the call, with microseconds precision.
'duration',
// One of enumStatuses above.
'status',
// Have we reminded them to log the call?
'log_reminder_sent_at',
// If activity is private or inter-org, flagged here.
'is_internal',
// Managers and above can mark a call as private, to exclude it from other team members
'is_private',
'is_processed',
// Boolean for this activity being instant invite handled.
'is_instant_invite',
// If activity is in recording state, flagged here.
'recording_state',
// If activity recording is overidden from default.
'recording_preference',
// if recording did (not) happen, why that is
'recording_reason_code',
// Average score, updated during
'average_score',
// Summary that the organizer has taken after the call.
'summary',
// Subject of the activity, usually taken from calendar event.
'title',
// Description of the activity, usually taken from calendar event.
'description',
// Start time, usually taken from calendar event.
'scheduled_start_time',
// End time, usually taken from calendar event.
'scheduled_end_time',
// When the call actually started.
'actual_start_time',
// When the call actually ended.
'actual_end_time',
// SMS: Message reference
'telephony_provider_id',
// SMS: Participant who sent message
'from_participant_id',
// SMS: Participant who should receive the message
'to_participant_id',
// When an external guest joins an organizers meeting room and the organizer is not present,
// send them an SMS notification that someone has joined.
'organizer_notified_at',
// where was the activity imported from
'source',
// The id in the source system (e.g. the bot id in Recall.ai)
'external_id',
// The provider, by default it is twilio.
'provider',
// Meeting location url
'location',
// The snapshot for displaying a poster image.
'poster_path',
'crm_configuration_id',
// If there is an automated message that the conversation is being recorded
'has_recording_prompt',
// If the activity is being live-streamed
'on_air',
'calendar_event_id',
];
protected $appends = [
'id_string',
'organizer',
];
protected $hidden = [
'uuid',
];
protected $visible = [
'id_string',
'type',
'duration',
'average_score',
'status',
'log_reminder_sent_at',
'title',
'description',
'is_internal',
'scheduled_start_time',
'scheduled_end_time',
'actual_start_time',
'actual_end_time',
'user',
'category',
'account',
'contact',
'opportunity',
'lead',
'stage',
'stats',
'participants',
'playlists',
'tracks',
'comments',
'plays',
'coachingFeedbacks',
'shares',
'favorites',
'language',
'transcription',
'is_private',
'is_instant_invite',
'on_air',
'calendar_event_id',
];
protected function casts(): array
{
return [
'scheduled_start_time' => 'datetime',
'scheduled_end_time' => 'datetime',
'actual_start_time' => 'datetime',
'actual_end_time' => 'datetime',
'organizer_notified_at' => 'datetime',
'log_reminder_sent_at' => 'datetime',
'is_internal' => 'boolean',
'duration' => 'integer',
'average_score' => 'decimal:2',
'is_private' => 'boolean',
'is_processed' => 'boolean',
'is_instant_invite' => 'boolean',
'value' => 'decimal:2',
'recording_preference' => 'boolean',
'recording_reason_code' => 'integer',
'has_recording_prompt' => 'boolean',
'on_air' => 'integer',
];
}
protected static function boot()
{
parent::boot();
static::updated(static function (Activity $activity) {
// If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week
if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||
($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {
if ($activity->isDirty('status')) {
event(new StatusUpdated($activity));
}
if ($activity->isDirty('stage_id')) {
event(new StageUpdated($activity));
}
if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {
event(new ProspectUpdated($activity));
}
if ($activity->isDirty('opportunity_id')) {
event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));
}
if ($activity->isDirty('title')) {
event(new TitleUpdated($activity));
}
}
if ($activity->isDirty('playbook_category_id')) {
event(new ActivityTypeUpdated($activity));
}
});
static::deleted(static function (Activity $activity) {
// Hard delete associated playlistActivities
$activity->playlistActivities()->delete();
});
}
public function getOrganizerAttribute(): ?Participant
{
$participant = $this->participants()->where('user_id', $this->user_id)->first();
if (! $participant instanceof Participant && $participant !== null) {
throw new RuntimeException(sprintf('$participant must be an instance of "%s" or null', Participant::class));
}
return $participant;
}
public function getFormattedValueAttribute()
{
$currencyCode = 'USD';
if ($this->opportunity) {
$currencyCode = $this->opportunity->getCurrencyCode();
}
$formatter = new CurrencyFormatter();
$formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);
$formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);
return $formatter->format($this->value, $currencyCode);
}
public function getProspectNameAttribute(): ?string
{
$prospectName = null;
if ($this->lead_id) {
$prospectName = $this->lead->name;
} elseif ($this->contact_id) {
$prospectName = $this->contact->name;
} elseif ($this->account_id) {
...
|
43033
|
NULL
|
NULL
|
NULL
|
|
43033
|
1571
|
48
|
2026-05-14T12:15:16.639639+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760916639_m1.jpg...
|
PhpStorm
|
faVsco.js – ImportParticipants.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Calendar\Command;
use Exception;
use Illuminate\Events\Dispatcher;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Cache;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Events\Activities\AiAutomation\ActivityProspectAdded;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Stage;
use Jiminny\Models\Participant;
use Jiminny\Models\User;
use Jiminny\Services\Calendar\Adapter\EventAttendee;
use Jiminny\Services\Calendar\Adapter\PersistedEventAttendee;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Jiminny\Services\Crm\MatchDomainByEmailInterface;
use Jiminny\Services\Crm\ProspectSearchStrategyFactory;
use Jiminny\Services\Crm\RecordSelector;
use Jiminny\Utils\StringUtil;
class ImportParticipants
{
private User $user;
private Activity $activity;
private MatchDomainByEmailInterface|ServiceInterface|null $crmService = null;
/**
* @var array<EventAttendee>
*/
private array $attendees;
private EmailHelper $emailHelper;
private TeamRepository $teamRepository;
private Dispatcher $eventDispatcher;
private array $emailRecords = [];
private array $domainRecords = [];
private array $participantsToKeep = [];
private bool $participantsChanged = false;
public function __construct(
User $user,
Activity $activity,
array $attendees,
EmailHelper $emailHelper,
TeamRepository $teamRepository,
Dispatcher $eventDispatcher,
) {
$this->user = $user;
$this->activity = $activity;
$this->attendees = $attendees;
$this->emailHelper = $emailHelper;
$this->teamRepository = $teamRepository;
$this->eventDispatcher = $eventDispatcher;
}
public function setCrmService(?ServiceInterface $crmService = null): void
{
$this->crmService = $crmService;
}
public function execute(bool $isNewActivity = false): void
{
// save in db and track if participants changed
$this->saveParticipants();
$participantsWereRemoved = false;
if ($this->activity->isScheduled()) {
// Delete the participants who are no longer invited or since declined.
$participantsWereRemoved = $this->purgeRemovedAndDeclinedParticipants();
}
// Only do CRM lookup if this is a new activity or participants changed
$shouldProcessCrm = $isNewActivity || $this->participantsChanged || $participantsWereRemoved;
if ($shouldProcessCrm) {
Log::info('[Calendar] Processing CRM lookup due to participant changes', [
'activity_id' => $this->activity->getUuid(),
'is_new_activity' => $isNewActivity,
'participants_changed' => $this->participantsChanged,
'participants_removed' => $participantsWereRemoved,
]);
// participants crm lookup
$this->processParticipants();
// update activity with crm results
$this->updateActivityCrmData($this->getRecords());
} else {
Log::info('[Calendar] Skipping CRM lookup - no participant changes', [
'activity_id' => $this->activity->getUuid(),
]);
}
}
public function updateActivityCrmData(array $records): void
{
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($this->user->getTeam());
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
// Unset any associated crm Data
Log::info('[Calendar] Ignoring crm data because of prospect strategy', [
'activity_id' => $this->activity->getUuid(),
'strategy' => get_class($prospectSearchStrategy),
]);
// empty the records
$records = [];
}
if (! empty($records)) {
$originalActivityData = $this->activity->getActivityProspectData();
$this->activity->updateActivityCrmData($records);
$this->activity->refresh();
$currentActivityData = $this->activity->getActivityProspectData();
Log::info('[Calendar] Updated activity crm data', [
'activity_id' => $this->activity->getUuid(),
'original_data' => $originalActivityData,
'current_data' => $currentActivityData,
]);
$this->eventDispatcher->dispatch(
new ActivityProspectAdded($this->activity, 'import-participants')
);
}
}
private function getRecords(): array
{
return RecordSelector::pickBestFromLists($this->emailRecords, $this->domainRecords);
}
private function shouldBeProcessed(EventAttendee $attendee): bool
{
// Ensure they are not a meeting resource e.g. a room and have a valid email.
return $attendee->isPerson()
&& filter_var($attendee->email(), FILTER_VALIDATE_EMAIL);
}
private function saveParticipants(): void
{
foreach ($this->attendees as $attendee) {
if (! $this->shouldBeProcessed($attendee)) {
continue;
}
$this->saveParticipant($attendee);
}
}
private function processParticipants(): void
{
foreach ($this->attendees as $attendee) {
if (! $this->shouldBeProcessed($attendee)) {
continue;
}
$this->processAttendee($attendee);
}
}
/**
* PersistedEventAttendee $persistedAttendee here is already saved in the DB participant
*
* @return void
*/
public function refreshCrmData(): void
{
/**
* @var PersistedEventAttendee $persistedAttendee
*/
foreach ($this->attendees as $persistedAttendee) {
$user = $persistedAttendee->getUser();
if ($user === null) {
$user = $this->refreshParticipantUserId($persistedAttendee);
}
$this->participantCrmLookup($persistedAttendee, $user);
}
$this->updateActivityCrmData($this->getRecords());
}
/**
* Save attendee in the DB as a participant
*
* @param EventAttendee $attendee
*
* @return void
*/
private function saveParticipant(EventAttendee $attendee): void
{
$email = $attendee->email();
$lockKey = $this->activity->getUuid() . '_' . $email;
$lock = Cache::lock($lockKey, 1);
if (! $lock->get()) {
Log::warning('[Calendar] Participant already being processed', [
'activity_id' => $this->activity->getUuid(),
'email' => $email,
]);
return;
}
Log::info('[Calendar] Save attendee as a participant', [
'activity_id' => $this->activity->getUuid(),
'email' => $email,
]);
$user = $this->teamRepository->findActiveTeamMemberByEmail($this->user->getTeam(), $email);
// User can have multiple calendars in Google, calendar provider id matches the email
if ($user === null) {
$user = $this->teamRepository->findActiveTeamMemberByCalendarProviderId($this->user->getTeam(), $email);
}
$participantData = [
'user_id' => $user?->getId(),
'email' => $email,
'role' => $attendee->isOrganizer() ? Participant::ROLE_ORGANIZER : null,
];
$this->setParticipantName($participantData, $this->getAttendeeName($attendee, $user));
$participant = $this->activity->updateOrCreateParticipant($participantData, false);
$this->trackParticipantChanges($participant, $email);
if (! $attendee->isDeclined()) {
Log::info('[Calendar] Adding participant to keep list', [
'activity_id' => $this->activity->getUuid(),
'email' => $email,
'participant_id' => $participant->getId(),
]);
$this->participantsToKeep[] = $participant->getId();
}
$lock->release();
}
/**
* Perform a CRM lookup for a participant (attendee)
*
* @param EventAttendee $attendee
*
* @return void
*/
private function processAttendee(EventAttendee $attendee): void
{
$email = $attendee->email();
Log::info('[Calendar] Process attendee', [
'activity_id' => $this->activity->getUuid(),
'email' => $email,
]);
$user = $this->teamRepository->findActiveTeamMemberByEmail($this->user->getTeam(), $email);
$this->participantCrmLookup($attendee, $user);
}
/**
* Perform a CRM lookup and update participant data
*
* @param EventAttendee $attendee
* @param User|null $user
*
* @return void
*/
public function participantCrmLookup(EventAttendee $attendee, ?User $user): void
{
$email = $attendee->email();
$crmRecords = null;
// Don't attempt CRM lookup of attendee if the meeting is internal, or they are the organizer.
// We shouldn't attempt to match any attendees that belong to the user's organisation as well
if ($this->shouldPerformCrmLookup($attendee, $user)) {
Log::info('[Calendar] Prospect attendee found. Attempting to match to a CRM record', [
'activity_id' => $this->activity->getUuid(),
'email' => $email,
]);
$crmRecords = $this->findProspectDataByEmail($email);
}
if (! empty($crmRecords)) {
Log::info('[Calendar] Prospect attendee matched to a CRM record', [
'activity_id' => $this->activity->getUuid(),
'email' => $email,
]);
$participantData = [
'user_id' => $user?->getId(),
'email' => $email,
];
if (empty($this->getAttendeeName($attendee, $user))) {
Log::info('[Calendar] Update participant name from CRM data', [
'activity_id' => $this->activity->getUuid(),
'email' => $email,
]);
// override name from CRM
$this->setParticipantName($participantData, $this->extractNameFromCrmData($crmRecords));
}
// update and get participant
$participant = $this->activity->updateOrCreateParticipant($participantData, false);
Log::info('[Calendar] Updating participant CRM data', [
'activity_id' => $this->activity->getUuid(),
'email' => $email,
'participant_id' => $participant->getId(),
]);
// update participant data from CRM
$this->activity->updateParticipantCrmData($crmRecords, $participant);
}
}
/**
* Refresh Participant userId
* Use case: the participant has not been registered as a User in the time of meeting event creation
* Check for a new registered User with that email and update Participant userId if so
*
* @param PersistedEventAttendee $attendee
*
* @return User|null
*/
private function refreshParticipantUserId(PersistedEventAttendee $attendee): ?User
{
$user = $this->teamRepository->findActiveTeamMemberByEmail($this->user->getTeam(), $attendee->email());
if ($user !== null) {
$participant = $attendee->asParticipant();
$participant->user_id = $user->getId();
$participant->save();
}
return $user;
}
private function getAttendeeName(EventAttendee $attendee, ?User $user): ?string
{
// Attendee display name is an optional field
$name = StringUtil::trimToSize((string) $attendee->displayName(), 100);
if (empty($name)) {
$name = $user?->getName();
}
return $name;
}
private function setParticipantName(array &$participantData, ?string $name): void
{
// If we have a name, set it on the participant
if ($name) {
Log::info('[Calendar] Setting participant name', [
'activity_id' => $this->activity->getUuid(),
'email' => $participantData['email'] ?? null,
'name' => $name,
]);
$participantData['name'] = $name;
}
}
private function shouldPerformCrmLookup(EventAttendee $attendee, ?User $user): bool
{
return ! $attendee->isOrganizer()
&& ! $this->activity->isInternal()
&& ! $this->isOrganisationMember($attendee, $user);
}
private function isOrganisationMember(EventAttendee $attendee, ?User $teamMember): bool
{
return $teamMember instanceof User
|| $this->emailHelper->isCompanyEmail($this->user->getTeam(), $attendee->email());
}
/**
* @param string|null $email
*
* @return null|array{?Lead, ?Account, ?Opportunity, ?Contact, ?Stage, ?string}
*/
private function findProspectDataByEmail(?string $email): ?array
{
try {
// Determine if this participant matches a known lead/contact in the CRM.
$user = $this->activity->getUser();
$userId = $user->getId();
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($this->crmService);
$decorator->setConfiguration($user->getTeam()->getCrmConfiguration());
$records = $decorator->matchExactlyByEmail($email, $userId);
if (! empty($records)) {
$this->emailRecords[] = $records;
} else {
$records = $decorator->matchByDomain($email, $userId);
if (! empty($records)) {
$this->domainRecords[] = $records;
}
}
} catch (Exception $exception) {
// If the CRM account is invalid or without a token, just give up matching the attendee.
Log::warning('[Calendar] Failed to match attendee to a CRM record', [
'activityId' => $this->activity->getUuid(),
'email' => $email,
'reason' => $exception->getMessage(),
]);
$records = null;
}
return $records;
}
private function purgeRemovedAndDeclinedParticipants(): bool
{
$participants = $this->activity->participants()->whereNotIn('id', $this->participantsToKeep);
$emails = $participants->pluck('email')->toArray();
if (empty($emails)) {
return false;
}
Log::info('[Calendar] Purging removed and declined participants', [
'activity_id' => $this->activity->getUuid(),
'emails' => $emails,
]);
$participants->delete();
return true;
}
/**
* @param array{?Lead, ?Account, ?Opportunity, ?Contact, ?Stage, ?string} $records
*
* @return string|null
*/
private function extractNameFromCrmData(array $records): ?string
{
[$lead, , , $contact] = $records;
return $lead?->getName() ?? $contact?->getName();
}
/**
* Track if a participant was added or updated to determine if CRM lookup is needed.
* Sets $this->participantsChanged to true if the participant is new or has relevant attribute changes.
* Timestamp fields (created_at, updated_at, deleted_at) are excluded from change detection.
*/
private function trackParticipantChanges(Participant $participant, string $email): void
{
$relevantChanges = array_diff_key(
$participant->getChanges() ?? [],
array_flip(['created_at', 'updated_at', 'deleted_at'])
);
if ($participant->wasRecentlyCreated) {
$this->participantsChanged = true;
Log::info('[Calendar] New participant added', [
'activity_id' => $this->activity->getUuid(),
'email' => $email,
'participant_id' => $participant->getId(),
]);
return;
}
if (! empty($relevantChanges)) {
$this->participantsChanged = true;
Log::info('[Calendar] Participant updated', [
'activity_id' => $this->activity->getUuid(),
'email' => $email,
'participant_id' => $participant->getId(),
'changes' => $relevantChanges,
]);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Calendar\\Command;\n\nuse Exception;\nuse Illuminate\\Events\\Dispatcher;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Facades\\Cache;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Events\\Activities\\AiAutomation\\ActivityProspectAdded;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Calendar\\Adapter\\EventAttendee;\nuse Jiminny\\Services\\Calendar\\Adapter\\PersistedEventAttendee;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Jiminny\\Services\\Crm\\MatchDomainByEmailInterface;\nuse Jiminny\\Services\\Crm\\ProspectSearchStrategyFactory;\nuse Jiminny\\Services\\Crm\\RecordSelector;\nuse Jiminny\\Utils\\StringUtil;\n\nclass ImportParticipants\n{\n private User $user;\n private Activity $activity;\n private MatchDomainByEmailInterface|ServiceInterface|null $crmService = null;\n\n /**\n * @var array<EventAttendee>\n */\n private array $attendees;\n private EmailHelper $emailHelper;\n private TeamRepository $teamRepository;\n private Dispatcher $eventDispatcher;\n\n private array $emailRecords = [];\n private array $domainRecords = [];\n private array $participantsToKeep = [];\n private bool $participantsChanged = false;\n\n public function __construct(\n User $user,\n Activity $activity,\n array $attendees,\n EmailHelper $emailHelper,\n TeamRepository $teamRepository,\n Dispatcher $eventDispatcher,\n ) {\n $this->user = $user;\n $this->activity = $activity;\n $this->attendees = $attendees;\n $this->emailHelper = $emailHelper;\n $this->teamRepository = $teamRepository;\n $this->eventDispatcher = $eventDispatcher;\n }\n\n public function setCrmService(?ServiceInterface $crmService = null): void\n {\n $this->crmService = $crmService;\n }\n\n public function execute(bool $isNewActivity = false): void\n {\n // save in db and track if participants changed\n $this->saveParticipants();\n\n $participantsWereRemoved = false;\n if ($this->activity->isScheduled()) {\n // Delete the participants who are no longer invited or since declined.\n $participantsWereRemoved = $this->purgeRemovedAndDeclinedParticipants();\n }\n\n // Only do CRM lookup if this is a new activity or participants changed\n $shouldProcessCrm = $isNewActivity || $this->participantsChanged || $participantsWereRemoved;\n\n if ($shouldProcessCrm) {\n Log::info('[Calendar] Processing CRM lookup due to participant changes', [\n 'activity_id' => $this->activity->getUuid(),\n 'is_new_activity' => $isNewActivity,\n 'participants_changed' => $this->participantsChanged,\n 'participants_removed' => $participantsWereRemoved,\n ]);\n\n // participants crm lookup\n $this->processParticipants();\n\n // update activity with crm results\n $this->updateActivityCrmData($this->getRecords());\n } else {\n Log::info('[Calendar] Skipping CRM lookup - no participant changes', [\n 'activity_id' => $this->activity->getUuid(),\n ]);\n }\n }\n\n public function updateActivityCrmData(array $records): void\n {\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($this->user->getTeam());\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n // Unset any associated crm Data\n Log::info('[Calendar] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $this->activity->getUuid(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n // empty the records\n $records = [];\n }\n\n if (! empty($records)) {\n $originalActivityData = $this->activity->getActivityProspectData();\n $this->activity->updateActivityCrmData($records);\n\n $this->activity->refresh();\n $currentActivityData = $this->activity->getActivityProspectData();\n\n Log::info('[Calendar] Updated activity crm data', [\n 'activity_id' => $this->activity->getUuid(),\n 'original_data' => $originalActivityData,\n 'current_data' => $currentActivityData,\n ]);\n\n $this->eventDispatcher->dispatch(\n new ActivityProspectAdded($this->activity, 'import-participants')\n );\n }\n }\n\n private function getRecords(): array\n {\n return RecordSelector::pickBestFromLists($this->emailRecords, $this->domainRecords);\n }\n\n private function shouldBeProcessed(EventAttendee $attendee): bool\n {\n // Ensure they are not a meeting resource e.g. a room and have a valid email.\n return $attendee->isPerson()\n && filter_var($attendee->email(), FILTER_VALIDATE_EMAIL);\n }\n\n private function saveParticipants(): void\n {\n foreach ($this->attendees as $attendee) {\n if (! $this->shouldBeProcessed($attendee)) {\n continue;\n }\n\n $this->saveParticipant($attendee);\n }\n }\n\n private function processParticipants(): void\n {\n foreach ($this->attendees as $attendee) {\n if (! $this->shouldBeProcessed($attendee)) {\n continue;\n }\n\n $this->processAttendee($attendee);\n }\n }\n\n /**\n * PersistedEventAttendee $persistedAttendee here is already saved in the DB participant\n *\n * @return void\n */\n public function refreshCrmData(): void\n {\n /**\n * @var PersistedEventAttendee $persistedAttendee\n */\n foreach ($this->attendees as $persistedAttendee) {\n $user = $persistedAttendee->getUser();\n\n if ($user === null) {\n $user = $this->refreshParticipantUserId($persistedAttendee);\n }\n\n $this->participantCrmLookup($persistedAttendee, $user);\n }\n\n $this->updateActivityCrmData($this->getRecords());\n }\n\n /**\n * Save attendee in the DB as a participant\n *\n * @param EventAttendee $attendee\n *\n * @return void\n */\n private function saveParticipant(EventAttendee $attendee): void\n {\n $email = $attendee->email();\n\n $lockKey = $this->activity->getUuid() . '_' . $email;\n $lock = Cache::lock($lockKey, 1);\n\n if (! $lock->get()) {\n Log::warning('[Calendar] Participant already being processed', [\n 'activity_id' => $this->activity->getUuid(),\n 'email' => $email,\n ]);\n\n return;\n }\n\n Log::info('[Calendar] Save attendee as a participant', [\n 'activity_id' => $this->activity->getUuid(),\n 'email' => $email,\n ]);\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($this->user->getTeam(), $email);\n\n // User can have multiple calendars in Google, calendar provider id matches the email\n if ($user === null) {\n $user = $this->teamRepository->findActiveTeamMemberByCalendarProviderId($this->user->getTeam(), $email);\n }\n\n $participantData = [\n 'user_id' => $user?->getId(),\n 'email' => $email,\n 'role' => $attendee->isOrganizer() ? Participant::ROLE_ORGANIZER : null,\n ];\n $this->setParticipantName($participantData, $this->getAttendeeName($attendee, $user));\n\n $participant = $this->activity->updateOrCreateParticipant($participantData, false);\n\n $this->trackParticipantChanges($participant, $email);\n\n if (! $attendee->isDeclined()) {\n Log::info('[Calendar] Adding participant to keep list', [\n 'activity_id' => $this->activity->getUuid(),\n 'email' => $email,\n 'participant_id' => $participant->getId(),\n ]);\n\n $this->participantsToKeep[] = $participant->getId();\n }\n\n $lock->release();\n }\n\n /**\n * Perform a CRM lookup for a participant (attendee)\n *\n * @param EventAttendee $attendee\n *\n * @return void\n */\n private function processAttendee(EventAttendee $attendee): void\n {\n $email = $attendee->email();\n\n Log::info('[Calendar] Process attendee', [\n 'activity_id' => $this->activity->getUuid(),\n 'email' => $email,\n ]);\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($this->user->getTeam(), $email);\n\n $this->participantCrmLookup($attendee, $user);\n }\n\n /**\n * Perform a CRM lookup and update participant data\n *\n * @param EventAttendee $attendee\n * @param User|null $user\n *\n * @return void\n */\n public function participantCrmLookup(EventAttendee $attendee, ?User $user): void\n {\n $email = $attendee->email();\n $crmRecords = null;\n\n // Don't attempt CRM lookup of attendee if the meeting is internal, or they are the organizer.\n // We shouldn't attempt to match any attendees that belong to the user's organisation as well\n if ($this->shouldPerformCrmLookup($attendee, $user)) {\n Log::info('[Calendar] Prospect attendee found. Attempting to match to a CRM record', [\n 'activity_id' => $this->activity->getUuid(),\n 'email' => $email,\n ]);\n\n $crmRecords = $this->findProspectDataByEmail($email);\n }\n\n if (! empty($crmRecords)) {\n Log::info('[Calendar] Prospect attendee matched to a CRM record', [\n 'activity_id' => $this->activity->getUuid(),\n 'email' => $email,\n ]);\n\n $participantData = [\n 'user_id' => $user?->getId(),\n 'email' => $email,\n ];\n\n if (empty($this->getAttendeeName($attendee, $user))) {\n Log::info('[Calendar] Update participant name from CRM data', [\n 'activity_id' => $this->activity->getUuid(),\n 'email' => $email,\n ]);\n\n // override name from CRM\n $this->setParticipantName($participantData, $this->extractNameFromCrmData($crmRecords));\n }\n\n // update and get participant\n $participant = $this->activity->updateOrCreateParticipant($participantData, false);\n\n Log::info('[Calendar] Updating participant CRM data', [\n 'activity_id' => $this->activity->getUuid(),\n 'email' => $email,\n 'participant_id' => $participant->getId(),\n ]);\n\n // update participant data from CRM\n $this->activity->updateParticipantCrmData($crmRecords, $participant);\n }\n }\n\n /**\n * Refresh Participant userId\n * Use case: the participant has not been registered as a User in the time of meeting event creation\n * Check for a new registered User with that email and update Participant userId if so\n *\n * @param PersistedEventAttendee $attendee\n *\n * @return User|null\n */\n private function refreshParticipantUserId(PersistedEventAttendee $attendee): ?User\n {\n $user = $this->teamRepository->findActiveTeamMemberByEmail($this->user->getTeam(), $attendee->email());\n\n if ($user !== null) {\n $participant = $attendee->asParticipant();\n $participant->user_id = $user->getId();\n $participant->save();\n }\n\n return $user;\n }\n\n private function getAttendeeName(EventAttendee $attendee, ?User $user): ?string\n {\n // Attendee display name is an optional field\n $name = StringUtil::trimToSize((string) $attendee->displayName(), 100);\n if (empty($name)) {\n $name = $user?->getName();\n }\n\n return $name;\n }\n\n private function setParticipantName(array &$participantData, ?string $name): void\n {\n // If we have a name, set it on the participant\n if ($name) {\n Log::info('[Calendar] Setting participant name', [\n 'activity_id' => $this->activity->getUuid(),\n 'email' => $participantData['email'] ?? null,\n 'name' => $name,\n ]);\n\n $participantData['name'] = $name;\n }\n }\n\n private function shouldPerformCrmLookup(EventAttendee $attendee, ?User $user): bool\n {\n return ! $attendee->isOrganizer()\n && ! $this->activity->isInternal()\n && ! $this->isOrganisationMember($attendee, $user);\n }\n\n private function isOrganisationMember(EventAttendee $attendee, ?User $teamMember): bool\n {\n return $teamMember instanceof User\n || $this->emailHelper->isCompanyEmail($this->user->getTeam(), $attendee->email());\n }\n\n /**\n * @param string|null $email\n *\n * @return null|array{?Lead, ?Account, ?Opportunity, ?Contact, ?Stage, ?string}\n */\n private function findProspectDataByEmail(?string $email): ?array\n {\n try {\n // Determine if this participant matches a known lead/contact in the CRM.\n $user = $this->activity->getUser();\n $userId = $user->getId();\n\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($this->crmService);\n $decorator->setConfiguration($user->getTeam()->getCrmConfiguration());\n\n $records = $decorator->matchExactlyByEmail($email, $userId);\n\n if (! empty($records)) {\n $this->emailRecords[] = $records;\n } else {\n $records = $decorator->matchByDomain($email, $userId);\n\n if (! empty($records)) {\n $this->domainRecords[] = $records;\n }\n }\n } catch (Exception $exception) {\n // If the CRM account is invalid or without a token, just give up matching the attendee.\n Log::warning('[Calendar] Failed to match attendee to a CRM record', [\n 'activityId' => $this->activity->getUuid(),\n 'email' => $email,\n 'reason' => $exception->getMessage(),\n ]);\n\n $records = null;\n }\n\n return $records;\n }\n\n private function purgeRemovedAndDeclinedParticipants(): bool\n {\n $participants = $this->activity->participants()->whereNotIn('id', $this->participantsToKeep);\n $emails = $participants->pluck('email')->toArray();\n\n if (empty($emails)) {\n return false;\n }\n\n Log::info('[Calendar] Purging removed and declined participants', [\n 'activity_id' => $this->activity->getUuid(),\n 'emails' => $emails,\n ]);\n\n $participants->delete();\n\n return true;\n }\n\n /**\n * @param array{?Lead, ?Account, ?Opportunity, ?Contact, ?Stage, ?string} $records\n *\n * @return string|null\n */\n private function extractNameFromCrmData(array $records): ?string\n {\n [$lead, , , $contact] = $records;\n\n return $lead?->getName() ?? $contact?->getName();\n }\n\n /**\n * Track if a participant was added or updated to determine if CRM lookup is needed.\n * Sets $this->participantsChanged to true if the participant is new or has relevant attribute changes.\n * Timestamp fields (created_at, updated_at, deleted_at) are excluded from change detection.\n */\n private function trackParticipantChanges(Participant $participant, string $email): void\n {\n $relevantChanges = array_diff_key(\n $participant->getChanges() ?? [],\n array_flip(['created_at', 'updated_at', 'deleted_at'])\n );\n\n if ($participant->wasRecentlyCreated) {\n $this->participantsChanged = true;\n\n Log::info('[Calendar] New participant added', [\n 'activity_id' => $this->activity->getUuid(),\n 'email' => $email,\n 'participant_id' => $participant->getId(),\n ]);\n\n return;\n }\n\n if (! empty($relevantChanges)) {\n $this->participantsChanged = true;\n\n Log::info('[Calendar] Participant updated', [\n 'activity_id' => $this->activity->getUuid(),\n 'email' => $email,\n 'participant_id' => $participant->getId(),\n 'changes' => $relevantChanges,\n ]);\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Calendar\\Command;\n\nuse Exception;\nuse Illuminate\\Events\\Dispatcher;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Facades\\Cache;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Events\\Activities\\AiAutomation\\ActivityProspectAdded;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Calendar\\Adapter\\EventAttendee;\nuse Jiminny\\Services\\Calendar\\Adapter\\PersistedEventAttendee;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Jiminny\\Services\\Crm\\MatchDomainByEmailInterface;\nuse Jiminny\\Services\\Crm\\ProspectSearchStrategyFactory;\nuse Jiminny\\Services\\Crm\\RecordSelector;\nuse Jiminny\\Utils\\StringUtil;\n\nclass ImportParticipants\n{\n private User $user;\n private Activity $activity;\n private MatchDomainByEmailInterface|ServiceInterface|null $crmService = null;\n\n /**\n * @var array<EventAttendee>\n */\n private array $attendees;\n private EmailHelper $emailHelper;\n private TeamRepository $teamRepository;\n private Dispatcher $eventDispatcher;\n\n private array $emailRecords = [];\n private array $domainRecords = [];\n private array $participantsToKeep = [];\n private bool $participantsChanged = false;\n\n public function __construct(\n User $user,\n Activity $activity,\n array $attendees,\n EmailHelper $emailHelper,\n TeamRepository $teamRepository,\n Dispatcher $eventDispatcher,\n ) {\n $this->user = $user;\n $this->activity = $activity;\n $this->attendees = $attendees;\n $this->emailHelper = $emailHelper;\n $this->teamRepository = $teamRepository;\n $this->eventDispatcher = $eventDispatcher;\n }\n\n public function setCrmService(?ServiceInterface $crmService = null): void\n {\n $this->crmService = $crmService;\n }\n\n public function execute(bool $isNewActivity = false): void\n {\n // save in db and track if participants changed\n $this->saveParticipants();\n\n $participantsWereRemoved = false;\n if ($this->activity->isScheduled()) {\n // Delete the participants who are no longer invited or since declined.\n $participantsWereRemoved = $this->purgeRemovedAndDeclinedParticipants();\n }\n\n // Only do CRM lookup if this is a new activity or participants changed\n $shouldProcessCrm = $isNewActivity || $this->participantsChanged || $participantsWereRemoved;\n\n if ($shouldProcessCrm) {\n Log::info('[Calendar] Processing CRM lookup due to participant changes', [\n 'activity_id' => $this->activity->getUuid(),\n 'is_new_activity' => $isNewActivity,\n 'participants_changed' => $this->participantsChanged,\n 'participants_removed' => $participantsWereRemoved,\n ]);\n\n // participants crm lookup\n $this->processParticipants();\n\n // update activity with crm results\n $this->updateActivityCrmData($this->getRecords());\n } else {\n Log::info('[Calendar] Skipping CRM lookup - no participant changes', [\n 'activity_id' => $this->activity->getUuid(),\n ]);\n }\n }\n\n public function updateActivityCrmData(array $records): void\n {\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($this->user->getTeam());\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n // Unset any associated crm Data\n Log::info('[Calendar] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $this->activity->getUuid(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n // empty the records\n $records = [];\n }\n\n if (! empty($records)) {\n $originalActivityData = $this->activity->getActivityProspectData();\n $this->activity->updateActivityCrmData($records);\n\n $this->activity->refresh();\n $currentActivityData = $this->activity->getActivityProspectData();\n\n Log::info('[Calendar] Updated activity crm data', [\n 'activity_id' => $this->activity->getUuid(),\n 'original_data' => $originalActivityData,\n 'current_data' => $currentActivityData,\n ]);\n\n $this->eventDispatcher->dispatch(\n new ActivityProspectAdded($this->activity, 'import-participants')\n );\n }\n }\n\n private function getRecords(): array\n {\n return RecordSelector::pickBestFromLists($this->emailRecords, $this->domainRecords);\n }\n\n private function shouldBeProcessed(EventAttendee $attendee): bool\n {\n // Ensure they are not a meeting resource e.g. a room and have a valid email.\n return $attendee->isPerson()\n && filter_var($attendee->email(), FILTER_VALIDATE_EMAIL);\n }\n\n private function saveParticipants(): void\n {\n foreach ($this->attendees as $attendee) {\n if (! $this->shouldBeProcessed($attendee)) {\n continue;\n }\n\n $this->saveParticipant($attendee);\n }\n }\n\n private function processParticipants(): void\n {\n foreach ($this->attendees as $attendee) {\n if (! $this->shouldBeProcessed($attendee)) {\n continue;\n }\n\n $this->processAttendee($attendee);\n }\n }\n\n /**\n * PersistedEventAttendee $persistedAttendee here is already saved in the DB participant\n *\n * @return void\n */\n public function refreshCrmData(): void\n {\n /**\n * @var PersistedEventAttendee $persistedAttendee\n */\n foreach ($this->attendees as $persistedAttendee) {\n $user = $persistedAttendee->getUser();\n\n if ($user === null) {\n $user = $this->refreshParticipantUserId($persistedAttendee);\n }\n\n $this->participantCrmLookup($persistedAttendee, $user);\n }\n\n $this->updateActivityCrmData($this->getRecords());\n }\n\n /**\n * Save attendee in the DB as a participant\n *\n * @param EventAttendee $attendee\n *\n * @return void\n */\n private function saveParticipant(EventAttendee $attendee): void\n {\n $email = $attendee->email();\n\n $lockKey = $this->activity->getUuid() . '_' . $email;\n $lock = Cache::lock($lockKey, 1);\n\n if (! $lock->get()) {\n Log::warning('[Calendar] Participant already being processed', [\n 'activity_id' => $this->activity->getUuid(),\n 'email' => $email,\n ]);\n\n return;\n }\n\n Log::info('[Calendar] Save attendee as a participant', [\n 'activity_id' => $this->activity->getUuid(),\n 'email' => $email,\n ]);\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($this->user->getTeam(), $email);\n\n // User can have multiple calendars in Google, calendar provider id matches the email\n if ($user === null) {\n $user = $this->teamRepository->findActiveTeamMemberByCalendarProviderId($this->user->getTeam(), $email);\n }\n\n $participantData = [\n 'user_id' => $user?->getId(),\n 'email' => $email,\n 'role' => $attendee->isOrganizer() ? Participant::ROLE_ORGANIZER : null,\n ];\n $this->setParticipantName($participantData, $this->getAttendeeName($attendee, $user));\n\n $participant = $this->activity->updateOrCreateParticipant($participantData, false);\n\n $this->trackParticipantChanges($participant, $email);\n\n if (! $attendee->isDeclined()) {\n Log::info('[Calendar] Adding participant to keep list', [\n 'activity_id' => $this->activity->getUuid(),\n 'email' => $email,\n 'participant_id' => $participant->getId(),\n ]);\n\n $this->participantsToKeep[] = $participant->getId();\n }\n\n $lock->release();\n }\n\n /**\n * Perform a CRM lookup for a participant (attendee)\n *\n * @param EventAttendee $attendee\n *\n * @return void\n */\n private function processAttendee(EventAttendee $attendee): void\n {\n $email = $attendee->email();\n\n Log::info('[Calendar] Process attendee', [\n 'activity_id' => $this->activity->getUuid(),\n 'email' => $email,\n ]);\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($this->user->getTeam(), $email);\n\n $this->participantCrmLookup($attendee, $user);\n }\n\n /**\n * Perform a CRM lookup and update participant data\n *\n * @param EventAttendee $attendee\n * @param User|null $user\n *\n * @return void\n */\n public function participantCrmLookup(EventAttendee $attendee, ?User $user): void\n {\n $email = $attendee->email();\n $crmRecords = null;\n\n // Don't attempt CRM lookup of attendee if the meeting is internal, or they are the organizer.\n // We shouldn't attempt to match any attendees that belong to the user's organisation as well\n if ($this->shouldPerformCrmLookup($attendee, $user)) {\n Log::info('[Calendar] Prospect attendee found. Attempting to match to a CRM record', [\n 'activity_id' => $this->activity->getUuid(),\n 'email' => $email,\n ]);\n\n $crmRecords = $this->findProspectDataByEmail($email);\n }\n\n if (! empty($crmRecords)) {\n Log::info('[Calendar] Prospect attendee matched to a CRM record', [\n 'activity_id' => $this->activity->getUuid(),\n 'email' => $email,\n ]);\n\n $participantData = [\n 'user_id' => $user?->getId(),\n 'email' => $email,\n ];\n\n if (empty($this->getAttendeeName($attendee, $user))) {\n Log::info('[Calendar] Update participant name from CRM data', [\n 'activity_id' => $this->activity->getUuid(),\n 'email' => $email,\n ]);\n\n // override name from CRM\n $this->setParticipantName($participantData, $this->extractNameFromCrmData($crmRecords));\n }\n\n // update and get participant\n $participant = $this->activity->updateOrCreateParticipant($participantData, false);\n\n Log::info('[Calendar] Updating participant CRM data', [\n 'activity_id' => $this->activity->getUuid(),\n 'email' => $email,\n 'participant_id' => $participant->getId(),\n ]);\n\n // update participant data from CRM\n $this->activity->updateParticipantCrmData($crmRecords, $participant);\n }\n }\n\n /**\n * Refresh Participant userId\n * Use case: the participant has not been registered as a User in the time of meeting event creation\n * Check for a new registered User with that email and update Participant userId if so\n *\n * @param PersistedEventAttendee $attendee\n *\n * @return User|null\n */\n private function refreshParticipantUserId(PersistedEventAttendee $attendee): ?User\n {\n $user = $this->teamRepository->findActiveTeamMemberByEmail($this->user->getTeam(), $attendee->email());\n\n if ($user !== null) {\n $participant = $attendee->asParticipant();\n $participant->user_id = $user->getId();\n $participant->save();\n }\n\n return $user;\n }\n\n private function getAttendeeName(EventAttendee $attendee, ?User $user): ?string\n {\n // Attendee display name is an optional field\n $name = StringUtil::trimToSize((string) $attendee->displayName(), 100);\n if (empty($name)) {\n $name = $user?->getName();\n }\n\n return $name;\n }\n\n private function setParticipantName(array &$participantData, ?string $name): void\n {\n // If we have a name, set it on the participant\n if ($name) {\n Log::info('[Calendar] Setting participant name', [\n 'activity_id' => $this->activity->getUuid(),\n 'email' => $participantData['email'] ?? null,\n 'name' => $name,\n ]);\n\n $participantData['name'] = $name;\n }\n }\n\n private function shouldPerformCrmLookup(EventAttendee $attendee, ?User $user): bool\n {\n return ! $attendee->isOrganizer()\n && ! $this->activity->isInternal()\n && ! $this->isOrganisationMember($attendee, $user);\n }\n\n private function isOrganisationMember(EventAttendee $attendee, ?User $teamMember): bool\n {\n return $teamMember instanceof User\n || $this->emailHelper->isCompanyEmail($this->user->getTeam(), $attendee->email());\n }\n\n /**\n * @param string|null $email\n *\n * @return null|array{?Lead, ?Account, ?Opportunity, ?Contact, ?Stage, ?string}\n */\n private function findProspectDataByEmail(?string $email): ?array\n {\n try {\n // Determine if this participant matches a known lead/contact in the CRM.\n $user = $this->activity->getUser();\n $userId = $user->getId();\n\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($this->crmService);\n $decorator->setConfiguration($user->getTeam()->getCrmConfiguration());\n\n $records = $decorator->matchExactlyByEmail($email, $userId);\n\n if (! empty($records)) {\n $this->emailRecords[] = $records;\n } else {\n $records = $decorator->matchByDomain($email, $userId);\n\n if (! empty($records)) {\n $this->domainRecords[] = $records;\n }\n }\n } catch (Exception $exception) {\n // If the CRM account is invalid or without a token, just give up matching the attendee.\n Log::warning('[Calendar] Failed to match attendee to a CRM record', [\n 'activityId' => $this->activity->getUuid(),\n 'email' => $email,\n 'reason' => $exception->getMessage(),\n ]);\n\n $records = null;\n }\n\n return $records;\n }\n\n private function purgeRemovedAndDeclinedParticipants(): bool\n {\n $participants = $this->activity->participants()->whereNotIn('id', $this->participantsToKeep);\n $emails = $participants->pluck('email')->toArray();\n\n if (empty($emails)) {\n return false;\n }\n\n Log::info('[Calendar] Purging removed and declined participants', [\n 'activity_id' => $this->activity->getUuid(),\n 'emails' => $emails,\n ]);\n\n $participants->delete();\n\n return true;\n }\n\n /**\n * @param array{?Lead, ?Account, ?Opportunity, ?Contact, ?Stage, ?string} $records\n *\n * @return string|null\n */\n private function extractNameFromCrmData(array $records): ?string\n {\n [$lead, , , $contact] = $records;\n\n return $lead?->getName() ?? $contact?->getName();\n }\n\n /**\n * Track if a participant was added or updated to determine if CRM lookup is needed.\n * Sets $this->participantsChanged to true if the participant is new or has relevant attribute changes.\n * Timestamp fields (created_at, updated_at, deleted_at) are excluded from change detection.\n */\n private function trackParticipantChanges(Participant $participant, string $email): void\n {\n $relevantChanges = array_diff_key(\n $participant->getChanges() ?? [],\n array_flip(['created_at', 'updated_at', 'deleted_at'])\n );\n\n if ($participant->wasRecentlyCreated) {\n $this->participantsChanged = true;\n\n Log::info('[Calendar] New participant added', [\n 'activity_id' => $this->activity->getUuid(),\n 'email' => $email,\n 'participant_id' => $participant->getId(),\n ]);\n\n return;\n }\n\n if (! empty($relevantChanges)) {\n $this->participantsChanged = true;\n\n Log::info('[Calendar] Participant updated', [\n 'activity_id' => $this->activity->getUuid(),\n 'email' => $email,\n 'participant_id' => $participant->getId(),\n 'changes' => $relevantChanges,\n ]);\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"169","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"102","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
6769527949084127828
|
6963561001770493756
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Calendar\Command;
use Exception;
use Illuminate\Events\Dispatcher;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Cache;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Events\Activities\AiAutomation\ActivityProspectAdded;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Stage;
use Jiminny\Models\Participant;
use Jiminny\Models\User;
use Jiminny\Services\Calendar\Adapter\EventAttendee;
use Jiminny\Services\Calendar\Adapter\PersistedEventAttendee;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Jiminny\Services\Crm\MatchDomainByEmailInterface;
use Jiminny\Services\Crm\ProspectSearchStrategyFactory;
use Jiminny\Services\Crm\RecordSelector;
use Jiminny\Utils\StringUtil;
class ImportParticipants
{
private User $user;
private Activity $activity;
private MatchDomainByEmailInterface|ServiceInterface|null $crmService = null;
/**
* @var array<EventAttendee>
*/
private array $attendees;
private EmailHelper $emailHelper;
private TeamRepository $teamRepository;
private Dispatcher $eventDispatcher;
private array $emailRecords = [];
private array $domainRecords = [];
private array $participantsToKeep = [];
private bool $participantsChanged = false;
public function __construct(
User $user,
Activity $activity,
array $attendees,
EmailHelper $emailHelper,
TeamRepository $teamRepository,
Dispatcher $eventDispatcher,
) {
$this->user = $user;
$this->activity = $activity;
$this->attendees = $attendees;
$this->emailHelper = $emailHelper;
$this->teamRepository = $teamRepository;
$this->eventDispatcher = $eventDispatcher;
}
public function setCrmService(?ServiceInterface $crmService = null): void
{
$this->crmService = $crmService;
}
public function execute(bool $isNewActivity = false): void
{
// save in db and track if participants changed
$this->saveParticipants();
$participantsWereRemoved = false;
if ($this->activity->isScheduled()) {
// Delete the participants who are no longer invited or since declined.
$participantsWereRemoved = $this->purgeRemovedAndDeclinedParticipants();
}
// Only do CRM lookup if this is a new activity or participants changed
$shouldProcessCrm = $isNewActivity || $this->participantsChanged || $participantsWereRemoved;
if ($shouldProcessCrm) {
Log::info('[Calendar] Processing CRM lookup due to participant changes', [
'activity_id' => $this->activity->getUuid(),
'is_new_activity' => $isNewActivity,
'participants_changed' => $this->participantsChanged,
'participants_removed' => $participantsWereRemoved,
]);
// participants crm lookup
$this->processParticipants();
// update activity with crm results
$this->updateActivityCrmData($this->getRecords());
} else {
Log::info('[Calendar] Skipping CRM lookup - no participant changes', [
'activity_id' => $this->activity->getUuid(),
]);
}
}
public function updateActivityCrmData(array $records): void
{
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($this->user->getTeam());
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
// Unset any associated crm Data
Log::info('[Calendar] Ignoring crm data because of prospect strategy', [
'activity_id' => $this->activity->getUuid(),
'strategy' => get_class($prospectSearchStrategy),
]);
// empty the records
$records = [];
}
if (! empty($records)) {
$originalActivityData = $this->activity->getActivityProspectData();
$this->activity->updateActivityCrmData($records);
$this->activity->refresh();
$currentActivityData = $this->activity->getActivityProspectData();
Log::info('[Calendar] Updated activity crm data', [
'activity_id' => $this->activity->getUuid(),
'original_data' => $originalActivityData,
'current_data' => $currentActivityData,
]);
$this->eventDispatcher->dispatch(
new ActivityProspectAdded($this->activity, 'import-participants')
);
}
}
private function getRecords(): array
{
return RecordSelector::pickBestFromLists($this->emailRecords, $this->domainRecords);
}
private function shouldBeProcessed(EventAttendee $attendee): bool
{
// Ensure they are not a meeting resource e.g. a room and have a valid email.
return $attendee->isPerson()
&& filter_var($attendee->email(), FILTER_VALIDATE_EMAIL);
}
private function saveParticipants(): void
{
foreach ($this->attendees as $attendee) {
if (! $this->shouldBeProcessed($attendee)) {
continue;
}
$this->saveParticipant($attendee);
}
}
private function processParticipants(): void
{
foreach ($this->attendees as $attendee) {
if (! $this->shouldBeProcessed($attendee)) {
continue;
}
$this->processAttendee($attendee);
}
}
/**
* PersistedEventAttendee $persistedAttendee here is already saved in the DB participant
*
* @return void
*/
public function refreshCrmData(): void
{
/**
* @var PersistedEventAttendee $persistedAttendee
*/
foreach ($this->attendees as $persistedAttendee) {
$user = $persistedAttendee->getUser();
if ($user === null) {
$user = $this->refreshParticipantUserId($persistedAttendee);
}
$this->participantCrmLookup($persistedAttendee, $user);
}
$this->updateActivityCrmData($this->getRecords());
}
/**
* Save attendee in the DB as a participant
*
* @param EventAttendee $attendee
*
* @return void
*/
private function saveParticipant(EventAttendee $attendee): void
{
$email = $attendee->email();
$lockKey = $this->activity->getUuid() . '_' . $email;
$lock = Cache::lock($lockKey, 1);
if (! $lock->get()) {
Log::warning('[Calendar] Participant already being processed', [
'activity_id' => $this->activity->getUuid(),
'email' => $email,
]);
return;
}
Log::info('[Calendar] Save attendee as a participant', [
'activity_id' => $this->activity->getUuid(),
'email' => $email,
]);
$user = $this->teamRepository->findActiveTeamMemberByEmail($this->user->getTeam(), $email);
// User can have multiple calendars in Google, calendar provider id matches the email
if ($user === null) {
$user = $this->teamRepository->findActiveTeamMemberByCalendarProviderId($this->user->getTeam(), $email);
}
$participantData = [
'user_id' => $user?->getId(),
'email' => $email,
'role' => $attendee->isOrganizer() ? Participant::ROLE_ORGANIZER : null,
];
$this->setParticipantName($participantData, $this->getAttendeeName($attendee, $user));
$participant = $this->activity->updateOrCreateParticipant($participantData, false);
$this->trackParticipantChanges($participant, $email);
if (! $attendee->isDeclined()) {
Log::info('[Calendar] Adding participant to keep list', [
'activity_id' => $this->activity->getUuid(),
'email' => $email,
'participant_id' => $participant->getId(),
]);
$this->participantsToKeep[] = $participant->getId();
}
$lock->release();
}
/**
* Perform a CRM lookup for a participant (attendee)
*
* @param EventAttendee $attendee
*
* @return void
*/
private function processAttendee(EventAttendee $attendee): void
{
$email = $attendee->email();
Log::info('[Calendar] Process attendee', [
'activity_id' => $this->activity->getUuid(),
'email' => $email,
]);
$user = $this->teamRepository->findActiveTeamMemberByEmail($this->user->getTeam(), $email);
$this->participantCrmLookup($attendee, $user);
}
/**
* Perform a CRM lookup and update participant data
*
* @param EventAttendee $attendee
* @param User|null $user
*
* @return void
*/
public function participantCrmLookup(EventAttendee $attendee, ?User $user): void
{
$email = $attendee->email();
$crmRecords = null;
// Don't attempt CRM lookup of attendee if the meeting is internal, or they are the organizer.
// We shouldn't attempt to match any attendees that belong to the user's organisation as well
if ($this->shouldPerformCrmLookup($attendee, $user)) {
Log::info('[Calendar] Prospect attendee found. Attempting to match to a CRM record', [
'activity_id' => $this->activity->getUuid(),
'email' => $email,
]);
$crmRecords = $this->findProspectDataByEmail($email);
}
if (! empty($crmRecords)) {
Log::info('[Calendar] Prospect attendee matched to a CRM record', [
'activity_id' => $this->activity->getUuid(),
'email' => $email,
]);
$participantData = [
'user_id' => $user?->getId(),
'email' => $email,
];
if (empty($this->getAttendeeName($attendee, $user))) {
Log::info('[Calendar] Update participant name from CRM data', [
'activity_id' => $this->activity->getUuid(),
'email' => $email,
]);
// override name from CRM
$this->setParticipantName($participantData, $this->extractNameFromCrmData($crmRecords));
}
// update and get participant
$participant = $this->activity->updateOrCreateParticipant($participantData, false);
Log::info('[Calendar] Updating participant CRM data', [
'activity_id' => $this->activity->getUuid(),
'email' => $email,
'participant_id' => $participant->getId(),
]);
// update participant data from CRM
$this->activity->updateParticipantCrmData($crmRecords, $participant);
}
}
/**
* Refresh Participant userId
* Use case: the participant has not been registered as a User in the time of meeting event creation
* Check for a new registered User with that email and update Participant userId if so
*
* @param PersistedEventAttendee $attendee
*
* @return User|null
*/
private function refreshParticipantUserId(PersistedEventAttendee $attendee): ?User
{
$user = $this->teamRepository->findActiveTeamMemberByEmail($this->user->getTeam(), $attendee->email());
if ($user !== null) {
$participant = $attendee->asParticipant();
$participant->user_id = $user->getId();
$participant->save();
}
return $user;
}
private function getAttendeeName(EventAttendee $attendee, ?User $user): ?string
{
// Attendee display name is an optional field
$name = StringUtil::trimToSize((string) $attendee->displayName(), 100);
if (empty($name)) {
$name = $user?->getName();
}
return $name;
}
private function setParticipantName(array &$participantData, ?string $name): void
{
// If we have a name, set it on the participant
if ($name) {
Log::info('[Calendar] Setting participant name', [
'activity_id' => $this->activity->getUuid(),
'email' => $participantData['email'] ?? null,
'name' => $name,
]);
$participantData['name'] = $name;
}
}
private function shouldPerformCrmLookup(EventAttendee $attendee, ?User $user): bool
{
return ! $attendee->isOrganizer()
&& ! $this->activity->isInternal()
&& ! $this->isOrganisationMember($attendee, $user);
}
private function isOrganisationMember(EventAttendee $attendee, ?User $teamMember): bool
{
return $teamMember instanceof User
|| $this->emailHelper->isCompanyEmail($this->user->getTeam(), $attendee->email());
}
/**
* @param string|null $email
*
* @return null|array{?Lead, ?Account, ?Opportunity, ?Contact, ?Stage, ?string}
*/
private function findProspectDataByEmail(?string $email): ?array
{
try {
// Determine if this participant matches a known lead/contact in the CRM.
$user = $this->activity->getUser();
$userId = $user->getId();
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($this->crmService);
$decorator->setConfiguration($user->getTeam()->getCrmConfiguration());
$records = $decorator->matchExactlyByEmail($email, $userId);
if (! empty($records)) {
$this->emailRecords[] = $records;
} else {
$records = $decorator->matchByDomain($email, $userId);
if (! empty($records)) {
$this->domainRecords[] = $records;
}
}
} catch (Exception $exception) {
// If the CRM account is invalid or without a token, just give up matching the attendee.
Log::warning('[Calendar] Failed to match attendee to a CRM record', [
'activityId' => $this->activity->getUuid(),
'email' => $email,
'reason' => $exception->getMessage(),
]);
$records = null;
}
return $records;
}
private function purgeRemovedAndDeclinedParticipants(): bool
{
$participants = $this->activity->participants()->whereNotIn('id', $this->participantsToKeep);
$emails = $participants->pluck('email')->toArray();
if (empty($emails)) {
return false;
}
Log::info('[Calendar] Purging removed and declined participants', [
'activity_id' => $this->activity->getUuid(),
'emails' => $emails,
]);
$participants->delete();
return true;
}
/**
* @param array{?Lead, ?Account, ?Opportunity, ?Contact, ?Stage, ?string} $records
*
* @return string|null
*/
private function extractNameFromCrmData(array $records): ?string
{
[$lead, , , $contact] = $records;
return $lead?->getName() ?? $contact?->getName();
}
/**
* Track if a participant was added or updated to determine if CRM lookup is needed.
* Sets $this->participantsChanged to true if the participant is new or has relevant attribute changes.
* Timestamp fields (created_at, updated_at, deleted_at) are excluded from change detection.
*/
private function trackParticipantChanges(Participant $participant, string $email): void
{
$relevantChanges = array_diff_key(
$participant->getChanges() ?? [],
array_flip(['created_at', 'updated_at', 'deleted_at'])
);
if ($participant->wasRecentlyCreated) {
$this->participantsChanged = true;
Log::info('[Calendar] New participant added', [
'activity_id' => $this->activity->getUuid(),
'email' => $email,
'participant_id' => $participant->getId(),
]);
return;
}
if (! empty($relevantChanges)) {
$this->participantsChanged = true;
Log::info('[Calendar] Participant updated', [
'activity_id' => $this->activity->getUuid(),
'email' => $email,
'participant_id' => $participant->getId(),
'changes' => $relevantChanges,
]);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
43032
|
1572
|
71
|
2026-05-14T12:15:15.160394+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-14/1778 /Users/lukas/.screenpipe/data/data/2026-05-14/1778760915160_m2.jpg...
|
PhpStorm
|
faVsco.js – ImportParticipants.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Calendar\Command;
use Exception;
use Illuminate\Events\Dispatcher;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Cache;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Events\Activities\AiAutomation\ActivityProspectAdded;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Stage;
use Jiminny\Models\Participant;
use Jiminny\Models\User;
use Jiminny\Services\Calendar\Adapter\EventAttendee;
use Jiminny\Services\Calendar\Adapter\PersistedEventAttendee;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Jiminny\Services\Crm\MatchDomainByEmailInterface;
use Jiminny\Services\Crm\ProspectSearchStrategyFactory;
use Jiminny\Services\Crm\RecordSelector;
use Jiminny\Utils\StringUtil;
class ImportParticipants
{
private User $user;
private Activity $activity;
private MatchDomainByEmailInterface|ServiceInterface|null $crmService = null;
/**
* @var array<EventAttendee>
*/
private array $attendees;
private EmailHelper $emailHelper;
private TeamRepository $teamRepository;
private Dispatcher $eventDispatcher;
private array $emailRecords = [];
private array $domainRecords = [];
private array $participantsToKeep = [];
private bool $participantsChanged = false;
public function __construct(
User $user,
Activity $activity,
array $attendees,
EmailHelper $emailHelper,
TeamRepository $teamRepository,
Dispatcher $eventDispatcher,
) {
$this->user = $user;
$this->activity = $activity;
$this->attendees = $attendees;
$this->emailHelper = $emailHelper;
$this->teamRepository = $teamRepository;
$this->eventDispatcher = $eventDispatcher;
}
public function setCrmService(?ServiceInterface $crmService = null): void
{
$this->crmService = $crmService;
}
public function execute(bool $isNewActivity = false): void
{
// save in db and track if participants changed
$this->saveParticipants();
$participantsWereRemoved = false;
if ($this->activity->isScheduled()) {
// Delete the participants who are no longer invited or since declined.
$participantsWereRemoved = $this->purgeRemovedAndDeclinedParticipants();
}
// Only do CRM lookup if this is a new activity or participants changed
$shouldProcessCrm = $isNewActivity || $this->participantsChanged || $participantsWereRemoved;
if ($shouldProcessCrm) {
Log::info('[Calendar] Processing CRM lookup due to participant changes', [
'activity_id' => $this->activity->getUuid(),
'is_new_activity' => $isNewActivity,
'participants_changed' => $this->participantsChanged,
'participants_removed' => $participantsWereRemoved,
]);
// participants crm lookup
$this->processParticipants();
// update activity with crm results
$this->updateActivityCrmData($this->getRecords());
} else {
Log::info('[Calendar] Skipping CRM lookup - no participant changes', [
'activity_id' => $this->activity->getUuid(),
]);
}
}
public function updateActivityCrmData(array $records): void
{
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($this->user->getTeam());
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
// Unset any associated crm Data
Log::info('[Calendar] Ignoring crm data because of prospect strategy', [
'activity_id' => $this->activity->getUuid(),
'strategy' => get_class($prospectSearchStrategy),
]);
// empty the records
$records = [];
}
if (! empty($records)) {
$originalActivityData = $this->activity->getActivityProspectData();
$this->activity->updateActivityCrmData($records);
$this->activity->refresh();
$currentActivityData = $this->activity->getActivityProspectData();
Log::info('[Calendar] Updated activity crm data', [
'activity_id' => $this->activity->getUuid(),
'original_data' => $originalActivityData,
'current_data' => $currentActivityData,
]);
$this->eventDispatcher->dispatch(
new ActivityProspectAdded($this->activity, 'import-participants')
);
}
}
private function getRecords(): array
{
return RecordSelector::pickBestFromLists($this->emailRecords, $this->domainRecords);
}
private function shouldBeProcessed(EventAttendee $attendee): bool
{
// Ensure they are not a meeting resource e.g. a room and have a valid email.
return $attendee->isPerson()
&& filter_var($attendee->email(), FILTER_VALIDATE_EMAIL);
}
private function saveParticipants(): void
{
foreach ($this->attendees as $attendee) {
if (! $this->shouldBeProcessed($attendee)) {
continue;
}
$this->saveParticipant($attendee);
}
}
private function processParticipants(): void
{
foreach ($this->attendees as $attendee) {
if (! $this->shouldBeProcessed($attendee)) {
continue;
}
$this->processAttendee($attendee);
}
}
/**
* PersistedEventAttendee $persistedAttendee here is already saved in the DB participant
*
* @return void
*/
public function refreshCrmData(): void
{
/**
* @var PersistedEventAttendee $persistedAttendee
*/
foreach ($this->attendees as $persistedAttendee) {
$user = $persistedAttendee->getUser();
if ($user === null) {
$user = $this->refreshParticipantUserId($persistedAttendee);
}
$this->participantCrmLookup($persistedAttendee, $user);
}
$this->updateActivityCrmData($this->getRecords());
}
/**
* Save attendee in the DB as a participant
*
* @param EventAttendee $attendee
*
* @return void
*/
private function saveParticipant(EventAttendee $attendee): void
{
$email = $attendee->email();
$lockKey = $this->activity->getUuid() . '_' . $email;
$lock = Cache::lock($lockKey, 1);
if (! $lock->get()) {
Log::warning('[Calendar] Participant already being processed', [
'activity_id' => $this->activity->getUuid(),
'email' => $email,
]);
return;
}
Log::info('[Calendar] Save attendee as a participant', [
'activity_id' => $this->activity->getUuid(),
'email' => $email,
]);
$user = $this->teamRepository->findActiveTeamMemberByEmail($this->user->getTeam(), $email);
// User can have multiple calendars in Google, calendar provider id matches the email
if ($user === null) {
$user = $this->teamRepository->findActiveTeamMemberByCalendarProviderId($this->user->getTeam(), $email);
}
$participantData = [
'user_id' => $user?->getId(),
'email' => $email,
'role' => $attendee->isOrganizer() ? Participant::ROLE_ORGANIZER : null,
];
$this->setParticipantName($participantData, $this->getAttendeeName($attendee, $user));
$participant = $this->activity->updateOrCreateParticipant($participantData, false);
$this->trackParticipantChanges($participant, $email);
if (! $attendee->isDeclined()) {
Log::info('[Calendar] Adding participant to keep list', [
'activity_id' => $this->activity->getUuid(),
'email' => $email,
'participant_id' => $participant->getId(),
]);
$this->participantsToKeep[] = $participant->getId();
}
$lock->release();
}
/**
* Perform a CRM lookup for a participant (attendee)
*
* @param EventAttendee $attendee
*
* @return void
*/
private function processAttendee(EventAttendee $attendee): void
{
$email = $attendee->email();
Log::info('[Calendar] Process attendee', [
'activity_id' => $this->activity->getUuid(),
'email' => $email,
]);
$user = $this->teamRepository->findActiveTeamMemberByEmail($this->user->getTeam(), $email);
$this->participantCrmLookup($attendee, $user);
}
/**
* Perform a CRM lookup and update participant data
*
* @param EventAttendee $attendee
* @param User|null $user
*
* @return void
*/
public function participantCrmLookup(EventAttendee $attendee, ?User $user): void
{
$email = $attendee->email();
$crmRecords = null;
// Don't attempt CRM lookup of attendee if the meeting is internal, or they are the organizer.
// We shouldn't attempt to match any attendees that belong to the user's organisation as well
if ($this->shouldPerformCrmLookup($attendee, $user)) {
Log::info('[Calendar] Prospect attendee found. Attempting to match to a CRM record', [
'activity_id' => $this->activity->getUuid(),
'email' => $email,
]);
$crmRecords = $this->findProspectDataByEmail($email);
}
if (! empty($crmRecords)) {
Log::info('[Calendar] Prospect attendee matched to a CRM record', [
'activity_id' => $this->activity->getUuid(),
'email' => $email,
]);
$participantData = [
'user_id' => $user?->getId(),
'email' => $email,
];
if (empty($this->getAttendeeName($attendee, $user))) {
Log::info('[Calendar] Update participant name from CRM data', [
'activity_id' => $this->activity->getUuid(),
'email' => $email,
]);
// override name from CRM
$this->setParticipantName($participantData, $this->extractNameFromCrmData($crmRecords));
}
// update and get participant
$participant = $this->activity->updateOrCreateParticipant($participantData, false);
Log::info('[Calendar] Updating participant CRM data', [
'activity_id' => $this->activity->getUuid(),
'email' => $email,
'participant_id' => $participant->getId(),
]);
// update participant data from CRM
$this->activity->updateParticipantCrmData($crmRecords, $participant);
}
}
/**
* Refresh Participant userId
* Use case: the participant has not been registered as a User in the time of meeting event creation
* Check for a new registered User with that email and update Participant userId if so
*
* @param PersistedEventAttendee $attendee
*
* @return User|null
*/
private function refreshParticipantUserId(PersistedEventAttendee $attendee): ?User
{
$user = $this->teamRepository->findActiveTeamMemberByEmail($this->user->getTeam(), $attendee->email());
if ($user !== null) {
$participant = $attendee->asParticipant();
$participant->user_id = $user->getId();
$participant->save();
}
return $user;
}
private function getAttendeeName(EventAttendee $attendee, ?User $user): ?string
{
// Attendee display name is an optional field
$name = StringUtil::trimToSize((string) $attendee->displayName(), 100);
if (empty($name)) {
$name = $user?->getName();
}
return $name;
}
private function setParticipantName(array &$participantData, ?string $name): void
{
// If we have a name, set it on the participant
if ($name) {
Log::info('[Calendar] Setting participant name', [
'activity_id' => $this->activity->getUuid(),
'email' => $participantData['email'] ?? null,
'name' => $name,
]);
$participantData['name'] = $name;
}
}
private function shouldPerformCrmLookup(EventAttendee $attendee, ?User $user): bool
{
return ! $attendee->isOrganizer()
&& ! $this->activity->isInternal()
&& ! $this->isOrganisationMember($attendee, $user);
}
private function isOrganisationMember(EventAttendee $attendee, ?User $teamMember): bool
{
return $teamMember instanceof User
|| $this->emailHelper->isCompanyEmail($this->user->getTeam(), $attendee->email());
}
/**
* @param string|null $email
*
* @return null|array{?Lead, ?Account, ?Opportunity, ?Contact, ?Stage, ?string}
*/
private function findProspectDataByEmail(?string $email): ?array
{
try {
// Determine if this participant matches a known lead/contact in the CRM.
$user = $this->activity->getUser();
$userId = $user->getId();
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($this->crmService);
$decorator->setConfiguration($user->getTeam()->getCrmConfiguration());
$records = $decorator->matchExactlyByEmail($email, $userId);
if (! empty($records)) {
$this->emailRecords[] = $records;
} else {
$records = $decorator->matchByDomain($email, $userId);
if (! empty($records)) {
$this->domainRecords[] = $records;
}
}
} catch (Exception $exception) {
// If the CRM account is invalid or without a token, just give up matching the attendee.
Log::warning('[Calendar] Failed to match attendee to a CRM record', [
'activityId' => $this->activity->getUuid(),
'email' => $email,
'reason' => $exception->getMessage(),
]);
$records = null;
}
return $records;
}
private function purgeRemovedAndDeclinedParticipants(): bool
{
$participants = $this->activity->participants()->whereNotIn('id', $this->participantsToKeep);
$emails = $participants->pluck('email')->toArray();
if (empty($emails)) {
return false;
}
Log::info('[Calendar] Purging removed and declined participants', [
'activity_id' => $this->activity->getUuid(),
'emails' => $emails,
]);
$participants->delete();
return true;
}
/**
* @param array{?Lead, ?Account, ?Opportunity, ?Contact, ?Stage, ?string} $records
*
* @return string|null
*/
private function extractNameFromCrmData(array $records): ?string
{
[$lead, , , $contact] = $records;
return $lead?->getName() ?? $contact?->getName();
}
/**
* Track if a participant was added or updated to determine if CRM lookup is needed.
* Sets $this->participantsChanged to true if the participant is new or has relevant attribute changes.
* Timestamp fields (created_at, updated_at, deleted_at) are excluded from change detection.
*/
private function trackParticipantChanges(Participant $participant, string $email): void
{
$relevantChanges = array_diff_key(
$participant->getChanges() ?? [],
array_flip(['created_at', 'updated_at', 'deleted_at'])
);
if ($participant->wasRecentlyCreated) {
$this->participantsChanged = true;
Log::info('[Calendar] New participant added', [
'activity_id' => $this->activity->getUuid(),
'email' => $email,
'participant_id' => $participant->getId(),
]);
return;
}
if (! empty($relevantChanges)) {
$this->participantsChanged = true;
Log::info('[Calendar] Participant updated', [
'activity_id' => $this->activity->getUuid(),
'email' => $email,
'participant_id' => $participant->getId(),
'changes' => $relevantChanges,
]);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"#12077 on JY-20903-update_activity-stage-on…hange, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.12566489,"height":0.025538707},"on_screen":true,"help_text":"Pull request #12077 exists for current branch JY-20903-update__activity-stage-on…hange","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.37466756,"top":0.07581804,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.38464096,"top":0.07581804,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.39361703,"top":0.074221864,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.40093085,"top":0.074221864,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Calendar\\Command;\n\nuse Exception;\nuse Illuminate\\Events\\Dispatcher;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Facades\\Cache;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Events\\Activities\\AiAutomation\\ActivityProspectAdded;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Calendar\\Adapter\\EventAttendee;\nuse Jiminny\\Services\\Calendar\\Adapter\\PersistedEventAttendee;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Jiminny\\Services\\Crm\\MatchDomainByEmailInterface;\nuse Jiminny\\Services\\Crm\\ProspectSearchStrategyFactory;\nuse Jiminny\\Services\\Crm\\RecordSelector;\nuse Jiminny\\Utils\\StringUtil;\n\nclass ImportParticipants\n{\n private User $user;\n private Activity $activity;\n private MatchDomainByEmailInterface|ServiceInterface|null $crmService = null;\n\n /**\n * @var array<EventAttendee>\n */\n private array $attendees;\n private EmailHelper $emailHelper;\n private TeamRepository $teamRepository;\n private Dispatcher $eventDispatcher;\n\n private array $emailRecords = [];\n private array $domainRecords = [];\n private array $participantsToKeep = [];\n private bool $participantsChanged = false;\n\n public function __construct(\n User $user,\n Activity $activity,\n array $attendees,\n EmailHelper $emailHelper,\n TeamRepository $teamRepository,\n Dispatcher $eventDispatcher,\n ) {\n $this->user = $user;\n $this->activity = $activity;\n $this->attendees = $attendees;\n $this->emailHelper = $emailHelper;\n $this->teamRepository = $teamRepository;\n $this->eventDispatcher = $eventDispatcher;\n }\n\n public function setCrmService(?ServiceInterface $crmService = null): void\n {\n $this->crmService = $crmService;\n }\n\n public function execute(bool $isNewActivity = false): void\n {\n // save in db and track if participants changed\n $this->saveParticipants();\n\n $participantsWereRemoved = false;\n if ($this->activity->isScheduled()) {\n // Delete the participants who are no longer invited or since declined.\n $participantsWereRemoved = $this->purgeRemovedAndDeclinedParticipants();\n }\n\n // Only do CRM lookup if this is a new activity or participants changed\n $shouldProcessCrm = $isNewActivity || $this->participantsChanged || $participantsWereRemoved;\n\n if ($shouldProcessCrm) {\n Log::info('[Calendar] Processing CRM lookup due to participant changes', [\n 'activity_id' => $this->activity->getUuid(),\n 'is_new_activity' => $isNewActivity,\n 'participants_changed' => $this->participantsChanged,\n 'participants_removed' => $participantsWereRemoved,\n ]);\n\n // participants crm lookup\n $this->processParticipants();\n\n // update activity with crm results\n $this->updateActivityCrmData($this->getRecords());\n } else {\n Log::info('[Calendar] Skipping CRM lookup - no participant changes', [\n 'activity_id' => $this->activity->getUuid(),\n ]);\n }\n }\n\n public function updateActivityCrmData(array $records): void\n {\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($this->user->getTeam());\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n // Unset any associated crm Data\n Log::info('[Calendar] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $this->activity->getUuid(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n // empty the records\n $records = [];\n }\n\n if (! empty($records)) {\n $originalActivityData = $this->activity->getActivityProspectData();\n $this->activity->updateActivityCrmData($records);\n\n $this->activity->refresh();\n $currentActivityData = $this->activity->getActivityProspectData();\n\n Log::info('[Calendar] Updated activity crm data', [\n 'activity_id' => $this->activity->getUuid(),\n 'original_data' => $originalActivityData,\n 'current_data' => $currentActivityData,\n ]);\n\n $this->eventDispatcher->dispatch(\n new ActivityProspectAdded($this->activity, 'import-participants')\n );\n }\n }\n\n private function getRecords(): array\n {\n return RecordSelector::pickBestFromLists($this->emailRecords, $this->domainRecords);\n }\n\n private function shouldBeProcessed(EventAttendee $attendee): bool\n {\n // Ensure they are not a meeting resource e.g. a room and have a valid email.\n return $attendee->isPerson()\n && filter_var($attendee->email(), FILTER_VALIDATE_EMAIL);\n }\n\n private function saveParticipants(): void\n {\n foreach ($this->attendees as $attendee) {\n if (! $this->shouldBeProcessed($attendee)) {\n continue;\n }\n\n $this->saveParticipant($attendee);\n }\n }\n\n private function processParticipants(): void\n {\n foreach ($this->attendees as $attendee) {\n if (! $this->shouldBeProcessed($attendee)) {\n continue;\n }\n\n $this->processAttendee($attendee);\n }\n }\n\n /**\n * PersistedEventAttendee $persistedAttendee here is already saved in the DB participant\n *\n * @return void\n */\n public function refreshCrmData(): void\n {\n /**\n * @var PersistedEventAttendee $persistedAttendee\n */\n foreach ($this->attendees as $persistedAttendee) {\n $user = $persistedAttendee->getUser();\n\n if ($user === null) {\n $user = $this->refreshParticipantUserId($persistedAttendee);\n }\n\n $this->participantCrmLookup($persistedAttendee, $user);\n }\n\n $this->updateActivityCrmData($this->getRecords());\n }\n\n /**\n * Save attendee in the DB as a participant\n *\n * @param EventAttendee $attendee\n *\n * @return void\n */\n private function saveParticipant(EventAttendee $attendee): void\n {\n $email = $attendee->email();\n\n $lockKey = $this->activity->getUuid() . '_' . $email;\n $lock = Cache::lock($lockKey, 1);\n\n if (! $lock->get()) {\n Log::warning('[Calendar] Participant already being processed', [\n 'activity_id' => $this->activity->getUuid(),\n 'email' => $email,\n ]);\n\n return;\n }\n\n Log::info('[Calendar] Save attendee as a participant', [\n 'activity_id' => $this->activity->getUuid(),\n 'email' => $email,\n ]);\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($this->user->getTeam(), $email);\n\n // User can have multiple calendars in Google, calendar provider id matches the email\n if ($user === null) {\n $user = $this->teamRepository->findActiveTeamMemberByCalendarProviderId($this->user->getTeam(), $email);\n }\n\n $participantData = [\n 'user_id' => $user?->getId(),\n 'email' => $email,\n 'role' => $attendee->isOrganizer() ? Participant::ROLE_ORGANIZER : null,\n ];\n $this->setParticipantName($participantData, $this->getAttendeeName($attendee, $user));\n\n $participant = $this->activity->updateOrCreateParticipant($participantData, false);\n\n $this->trackParticipantChanges($participant, $email);\n\n if (! $attendee->isDeclined()) {\n Log::info('[Calendar] Adding participant to keep list', [\n 'activity_id' => $this->activity->getUuid(),\n 'email' => $email,\n 'participant_id' => $participant->getId(),\n ]);\n\n $this->participantsToKeep[] = $participant->getId();\n }\n\n $lock->release();\n }\n\n /**\n * Perform a CRM lookup for a participant (attendee)\n *\n * @param EventAttendee $attendee\n *\n * @return void\n */\n private function processAttendee(EventAttendee $attendee): void\n {\n $email = $attendee->email();\n\n Log::info('[Calendar] Process attendee', [\n 'activity_id' => $this->activity->getUuid(),\n 'email' => $email,\n ]);\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($this->user->getTeam(), $email);\n\n $this->participantCrmLookup($attendee, $user);\n }\n\n /**\n * Perform a CRM lookup and update participant data\n *\n * @param EventAttendee $attendee\n * @param User|null $user\n *\n * @return void\n */\n public function participantCrmLookup(EventAttendee $attendee, ?User $user): void\n {\n $email = $attendee->email();\n $crmRecords = null;\n\n // Don't attempt CRM lookup of attendee if the meeting is internal, or they are the organizer.\n // We shouldn't attempt to match any attendees that belong to the user's organisation as well\n if ($this->shouldPerformCrmLookup($attendee, $user)) {\n Log::info('[Calendar] Prospect attendee found. Attempting to match to a CRM record', [\n 'activity_id' => $this->activity->getUuid(),\n 'email' => $email,\n ]);\n\n $crmRecords = $this->findProspectDataByEmail($email);\n }\n\n if (! empty($crmRecords)) {\n Log::info('[Calendar] Prospect attendee matched to a CRM record', [\n 'activity_id' => $this->activity->getUuid(),\n 'email' => $email,\n ]);\n\n $participantData = [\n 'user_id' => $user?->getId(),\n 'email' => $email,\n ];\n\n if (empty($this->getAttendeeName($attendee, $user))) {\n Log::info('[Calendar] Update participant name from CRM data', [\n 'activity_id' => $this->activity->getUuid(),\n 'email' => $email,\n ]);\n\n // override name from CRM\n $this->setParticipantName($participantData, $this->extractNameFromCrmData($crmRecords));\n }\n\n // update and get participant\n $participant = $this->activity->updateOrCreateParticipant($participantData, false);\n\n Log::info('[Calendar] Updating participant CRM data', [\n 'activity_id' => $this->activity->getUuid(),\n 'email' => $email,\n 'participant_id' => $participant->getId(),\n ]);\n\n // update participant data from CRM\n $this->activity->updateParticipantCrmData($crmRecords, $participant);\n }\n }\n\n /**\n * Refresh Participant userId\n * Use case: the participant has not been registered as a User in the time of meeting event creation\n * Check for a new registered User with that email and update Participant userId if so\n *\n * @param PersistedEventAttendee $attendee\n *\n * @return User|null\n */\n private function refreshParticipantUserId(PersistedEventAttendee $attendee): ?User\n {\n $user = $this->teamRepository->findActiveTeamMemberByEmail($this->user->getTeam(), $attendee->email());\n\n if ($user !== null) {\n $participant = $attendee->asParticipant();\n $participant->user_id = $user->getId();\n $participant->save();\n }\n\n return $user;\n }\n\n private function getAttendeeName(EventAttendee $attendee, ?User $user): ?string\n {\n // Attendee display name is an optional field\n $name = StringUtil::trimToSize((string) $attendee->displayName(), 100);\n if (empty($name)) {\n $name = $user?->getName();\n }\n\n return $name;\n }\n\n private function setParticipantName(array &$participantData, ?string $name): void\n {\n // If we have a name, set it on the participant\n if ($name) {\n Log::info('[Calendar] Setting participant name', [\n 'activity_id' => $this->activity->getUuid(),\n 'email' => $participantData['email'] ?? null,\n 'name' => $name,\n ]);\n\n $participantData['name'] = $name;\n }\n }\n\n private function shouldPerformCrmLookup(EventAttendee $attendee, ?User $user): bool\n {\n return ! $attendee->isOrganizer()\n && ! $this->activity->isInternal()\n && ! $this->isOrganisationMember($attendee, $user);\n }\n\n private function isOrganisationMember(EventAttendee $attendee, ?User $teamMember): bool\n {\n return $teamMember instanceof User\n || $this->emailHelper->isCompanyEmail($this->user->getTeam(), $attendee->email());\n }\n\n /**\n * @param string|null $email\n *\n * @return null|array{?Lead, ?Account, ?Opportunity, ?Contact, ?Stage, ?string}\n */\n private function findProspectDataByEmail(?string $email): ?array\n {\n try {\n // Determine if this participant matches a known lead/contact in the CRM.\n $user = $this->activity->getUser();\n $userId = $user->getId();\n\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($this->crmService);\n $decorator->setConfiguration($user->getTeam()->getCrmConfiguration());\n\n $records = $decorator->matchExactlyByEmail($email, $userId);\n\n if (! empty($records)) {\n $this->emailRecords[] = $records;\n } else {\n $records = $decorator->matchByDomain($email, $userId);\n\n if (! empty($records)) {\n $this->domainRecords[] = $records;\n }\n }\n } catch (Exception $exception) {\n // If the CRM account is invalid or without a token, just give up matching the attendee.\n Log::warning('[Calendar] Failed to match attendee to a CRM record', [\n 'activityId' => $this->activity->getUuid(),\n 'email' => $email,\n 'reason' => $exception->getMessage(),\n ]);\n\n $records = null;\n }\n\n return $records;\n }\n\n private function purgeRemovedAndDeclinedParticipants(): bool\n {\n $participants = $this->activity->participants()->whereNotIn('id', $this->participantsToKeep);\n $emails = $participants->pluck('email')->toArray();\n\n if (empty($emails)) {\n return false;\n }\n\n Log::info('[Calendar] Purging removed and declined participants', [\n 'activity_id' => $this->activity->getUuid(),\n 'emails' => $emails,\n ]);\n\n $participants->delete();\n\n return true;\n }\n\n /**\n * @param array{?Lead, ?Account, ?Opportunity, ?Contact, ?Stage, ?string} $records\n *\n * @return string|null\n */\n private function extractNameFromCrmData(array $records): ?string\n {\n [$lead, , , $contact] = $records;\n\n return $lead?->getName() ?? $contact?->getName();\n }\n\n /**\n * Track if a participant was added or updated to determine if CRM lookup is needed.\n * Sets $this->participantsChanged to true if the participant is new or has relevant attribute changes.\n * Timestamp fields (created_at, updated_at, deleted_at) are excluded from change detection.\n */\n private function trackParticipantChanges(Participant $participant, string $email): void\n {\n $relevantChanges = array_diff_key(\n $participant->getChanges() ?? [],\n array_flip(['created_at', 'updated_at', 'deleted_at'])\n );\n\n if ($participant->wasRecentlyCreated) {\n $this->participantsChanged = true;\n\n Log::info('[Calendar] New participant added', [\n 'activity_id' => $this->activity->getUuid(),\n 'email' => $email,\n 'participant_id' => $participant->getId(),\n ]);\n\n return;\n }\n\n if (! empty($relevantChanges)) {\n $this->participantsChanged = true;\n\n Log::info('[Calendar] Participant updated', [\n 'activity_id' => $this->activity->getUuid(),\n 'email' => $email,\n 'participant_id' => $participant->getId(),\n 'changes' => $relevantChanges,\n ]);\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Calendar\\Command;\n\nuse Exception;\nuse Illuminate\\Events\\Dispatcher;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Facades\\Cache;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Events\\Activities\\AiAutomation\\ActivityProspectAdded;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\Calendar\\Adapter\\EventAttendee;\nuse Jiminny\\Services\\Calendar\\Adapter\\PersistedEventAttendee;\nuse Jiminny\\Services\\Crm\\CachedCrmServiceDecorator;\nuse Jiminny\\Services\\Crm\\EmailHelper;\nuse Jiminny\\Services\\Crm\\MatchDomainByEmailInterface;\nuse Jiminny\\Services\\Crm\\ProspectSearchStrategyFactory;\nuse Jiminny\\Services\\Crm\\RecordSelector;\nuse Jiminny\\Utils\\StringUtil;\n\nclass ImportParticipants\n{\n private User $user;\n private Activity $activity;\n private MatchDomainByEmailInterface|ServiceInterface|null $crmService = null;\n\n /**\n * @var array<EventAttendee>\n */\n private array $attendees;\n private EmailHelper $emailHelper;\n private TeamRepository $teamRepository;\n private Dispatcher $eventDispatcher;\n\n private array $emailRecords = [];\n private array $domainRecords = [];\n private array $participantsToKeep = [];\n private bool $participantsChanged = false;\n\n public function __construct(\n User $user,\n Activity $activity,\n array $attendees,\n EmailHelper $emailHelper,\n TeamRepository $teamRepository,\n Dispatcher $eventDispatcher,\n ) {\n $this->user = $user;\n $this->activity = $activity;\n $this->attendees = $attendees;\n $this->emailHelper = $emailHelper;\n $this->teamRepository = $teamRepository;\n $this->eventDispatcher = $eventDispatcher;\n }\n\n public function setCrmService(?ServiceInterface $crmService = null): void\n {\n $this->crmService = $crmService;\n }\n\n public function execute(bool $isNewActivity = false): void\n {\n // save in db and track if participants changed\n $this->saveParticipants();\n\n $participantsWereRemoved = false;\n if ($this->activity->isScheduled()) {\n // Delete the participants who are no longer invited or since declined.\n $participantsWereRemoved = $this->purgeRemovedAndDeclinedParticipants();\n }\n\n // Only do CRM lookup if this is a new activity or participants changed\n $shouldProcessCrm = $isNewActivity || $this->participantsChanged || $participantsWereRemoved;\n\n if ($shouldProcessCrm) {\n Log::info('[Calendar] Processing CRM lookup due to participant changes', [\n 'activity_id' => $this->activity->getUuid(),\n 'is_new_activity' => $isNewActivity,\n 'participants_changed' => $this->participantsChanged,\n 'participants_removed' => $participantsWereRemoved,\n ]);\n\n // participants crm lookup\n $this->processParticipants();\n\n // update activity with crm results\n $this->updateActivityCrmData($this->getRecords());\n } else {\n Log::info('[Calendar] Skipping CRM lookup - no participant changes', [\n 'activity_id' => $this->activity->getUuid(),\n ]);\n }\n }\n\n public function updateActivityCrmData(array $records): void\n {\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($this->user->getTeam());\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n // Unset any associated crm Data\n Log::info('[Calendar] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $this->activity->getUuid(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n // empty the records\n $records = [];\n }\n\n if (! empty($records)) {\n $originalActivityData = $this->activity->getActivityProspectData();\n $this->activity->updateActivityCrmData($records);\n\n $this->activity->refresh();\n $currentActivityData = $this->activity->getActivityProspectData();\n\n Log::info('[Calendar] Updated activity crm data', [\n 'activity_id' => $this->activity->getUuid(),\n 'original_data' => $originalActivityData,\n 'current_data' => $currentActivityData,\n ]);\n\n $this->eventDispatcher->dispatch(\n new ActivityProspectAdded($this->activity, 'import-participants')\n );\n }\n }\n\n private function getRecords(): array\n {\n return RecordSelector::pickBestFromLists($this->emailRecords, $this->domainRecords);\n }\n\n private function shouldBeProcessed(EventAttendee $attendee): bool\n {\n // Ensure they are not a meeting resource e.g. a room and have a valid email.\n return $attendee->isPerson()\n && filter_var($attendee->email(), FILTER_VALIDATE_EMAIL);\n }\n\n private function saveParticipants(): void\n {\n foreach ($this->attendees as $attendee) {\n if (! $this->shouldBeProcessed($attendee)) {\n continue;\n }\n\n $this->saveParticipant($attendee);\n }\n }\n\n private function processParticipants(): void\n {\n foreach ($this->attendees as $attendee) {\n if (! $this->shouldBeProcessed($attendee)) {\n continue;\n }\n\n $this->processAttendee($attendee);\n }\n }\n\n /**\n * PersistedEventAttendee $persistedAttendee here is already saved in the DB participant\n *\n * @return void\n */\n public function refreshCrmData(): void\n {\n /**\n * @var PersistedEventAttendee $persistedAttendee\n */\n foreach ($this->attendees as $persistedAttendee) {\n $user = $persistedAttendee->getUser();\n\n if ($user === null) {\n $user = $this->refreshParticipantUserId($persistedAttendee);\n }\n\n $this->participantCrmLookup($persistedAttendee, $user);\n }\n\n $this->updateActivityCrmData($this->getRecords());\n }\n\n /**\n * Save attendee in the DB as a participant\n *\n * @param EventAttendee $attendee\n *\n * @return void\n */\n private function saveParticipant(EventAttendee $attendee): void\n {\n $email = $attendee->email();\n\n $lockKey = $this->activity->getUuid() . '_' . $email;\n $lock = Cache::lock($lockKey, 1);\n\n if (! $lock->get()) {\n Log::warning('[Calendar] Participant already being processed', [\n 'activity_id' => $this->activity->getUuid(),\n 'email' => $email,\n ]);\n\n return;\n }\n\n Log::info('[Calendar] Save attendee as a participant', [\n 'activity_id' => $this->activity->getUuid(),\n 'email' => $email,\n ]);\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($this->user->getTeam(), $email);\n\n // User can have multiple calendars in Google, calendar provider id matches the email\n if ($user === null) {\n $user = $this->teamRepository->findActiveTeamMemberByCalendarProviderId($this->user->getTeam(), $email);\n }\n\n $participantData = [\n 'user_id' => $user?->getId(),\n 'email' => $email,\n 'role' => $attendee->isOrganizer() ? Participant::ROLE_ORGANIZER : null,\n ];\n $this->setParticipantName($participantData, $this->getAttendeeName($attendee, $user));\n\n $participant = $this->activity->updateOrCreateParticipant($participantData, false);\n\n $this->trackParticipantChanges($participant, $email);\n\n if (! $attendee->isDeclined()) {\n Log::info('[Calendar] Adding participant to keep list', [\n 'activity_id' => $this->activity->getUuid(),\n 'email' => $email,\n 'participant_id' => $participant->getId(),\n ]);\n\n $this->participantsToKeep[] = $participant->getId();\n }\n\n $lock->release();\n }\n\n /**\n * Perform a CRM lookup for a participant (attendee)\n *\n * @param EventAttendee $attendee\n *\n * @return void\n */\n private function processAttendee(EventAttendee $attendee): void\n {\n $email = $attendee->email();\n\n Log::info('[Calendar] Process attendee', [\n 'activity_id' => $this->activity->getUuid(),\n 'email' => $email,\n ]);\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($this->user->getTeam(), $email);\n\n $this->participantCrmLookup($attendee, $user);\n }\n\n /**\n * Perform a CRM lookup and update participant data\n *\n * @param EventAttendee $attendee\n * @param User|null $user\n *\n * @return void\n */\n public function participantCrmLookup(EventAttendee $attendee, ?User $user): void\n {\n $email = $attendee->email();\n $crmRecords = null;\n\n // Don't attempt CRM lookup of attendee if the meeting is internal, or they are the organizer.\n // We shouldn't attempt to match any attendees that belong to the user's organisation as well\n if ($this->shouldPerformCrmLookup($attendee, $user)) {\n Log::info('[Calendar] Prospect attendee found. Attempting to match to a CRM record', [\n 'activity_id' => $this->activity->getUuid(),\n 'email' => $email,\n ]);\n\n $crmRecords = $this->findProspectDataByEmail($email);\n }\n\n if (! empty($crmRecords)) {\n Log::info('[Calendar] Prospect attendee matched to a CRM record', [\n 'activity_id' => $this->activity->getUuid(),\n 'email' => $email,\n ]);\n\n $participantData = [\n 'user_id' => $user?->getId(),\n 'email' => $email,\n ];\n\n if (empty($this->getAttendeeName($attendee, $user))) {\n Log::info('[Calendar] Update participant name from CRM data', [\n 'activity_id' => $this->activity->getUuid(),\n 'email' => $email,\n ]);\n\n // override name from CRM\n $this->setParticipantName($participantData, $this->extractNameFromCrmData($crmRecords));\n }\n\n // update and get participant\n $participant = $this->activity->updateOrCreateParticipant($participantData, false);\n\n Log::info('[Calendar] Updating participant CRM data', [\n 'activity_id' => $this->activity->getUuid(),\n 'email' => $email,\n 'participant_id' => $participant->getId(),\n ]);\n\n // update participant data from CRM\n $this->activity->updateParticipantCrmData($crmRecords, $participant);\n }\n }\n\n /**\n * Refresh Participant userId\n * Use case: the participant has not been registered as a User in the time of meeting event creation\n * Check for a new registered User with that email and update Participant userId if so\n *\n * @param PersistedEventAttendee $attendee\n *\n * @return User|null\n */\n private function refreshParticipantUserId(PersistedEventAttendee $attendee): ?User\n {\n $user = $this->teamRepository->findActiveTeamMemberByEmail($this->user->getTeam(), $attendee->email());\n\n if ($user !== null) {\n $participant = $attendee->asParticipant();\n $participant->user_id = $user->getId();\n $participant->save();\n }\n\n return $user;\n }\n\n private function getAttendeeName(EventAttendee $attendee, ?User $user): ?string\n {\n // Attendee display name is an optional field\n $name = StringUtil::trimToSize((string) $attendee->displayName(), 100);\n if (empty($name)) {\n $name = $user?->getName();\n }\n\n return $name;\n }\n\n private function setParticipantName(array &$participantData, ?string $name): void\n {\n // If we have a name, set it on the participant\n if ($name) {\n Log::info('[Calendar] Setting participant name', [\n 'activity_id' => $this->activity->getUuid(),\n 'email' => $participantData['email'] ?? null,\n 'name' => $name,\n ]);\n\n $participantData['name'] = $name;\n }\n }\n\n private function shouldPerformCrmLookup(EventAttendee $attendee, ?User $user): bool\n {\n return ! $attendee->isOrganizer()\n && ! $this->activity->isInternal()\n && ! $this->isOrganisationMember($attendee, $user);\n }\n\n private function isOrganisationMember(EventAttendee $attendee, ?User $teamMember): bool\n {\n return $teamMember instanceof User\n || $this->emailHelper->isCompanyEmail($this->user->getTeam(), $attendee->email());\n }\n\n /**\n * @param string|null $email\n *\n * @return null|array{?Lead, ?Account, ?Opportunity, ?Contact, ?Stage, ?string}\n */\n private function findProspectDataByEmail(?string $email): ?array\n {\n try {\n // Determine if this participant matches a known lead/contact in the CRM.\n $user = $this->activity->getUser();\n $userId = $user->getId();\n\n $decorator = resolve(CachedCrmServiceDecorator::class);\n $decorator->setCrmService($this->crmService);\n $decorator->setConfiguration($user->getTeam()->getCrmConfiguration());\n\n $records = $decorator->matchExactlyByEmail($email, $userId);\n\n if (! empty($records)) {\n $this->emailRecords[] = $records;\n } else {\n $records = $decorator->matchByDomain($email, $userId);\n\n if (! empty($records)) {\n $this->domainRecords[] = $records;\n }\n }\n } catch (Exception $exception) {\n // If the CRM account is invalid or without a token, just give up matching the attendee.\n Log::warning('[Calendar] Failed to match attendee to a CRM record', [\n 'activityId' => $this->activity->getUuid(),\n 'email' => $email,\n 'reason' => $exception->getMessage(),\n ]);\n\n $records = null;\n }\n\n return $records;\n }\n\n private function purgeRemovedAndDeclinedParticipants(): bool\n {\n $participants = $this->activity->participants()->whereNotIn('id', $this->participantsToKeep);\n $emails = $participants->pluck('email')->toArray();\n\n if (empty($emails)) {\n return false;\n }\n\n Log::info('[Calendar] Purging removed and declined participants', [\n 'activity_id' => $this->activity->getUuid(),\n 'emails' => $emails,\n ]);\n\n $participants->delete();\n\n return true;\n }\n\n /**\n * @param array{?Lead, ?Account, ?Opportunity, ?Contact, ?Stage, ?string} $records\n *\n * @return string|null\n */\n private function extractNameFromCrmData(array $records): ?string\n {\n [$lead, , , $contact] = $records;\n\n return $lead?->getName() ?? $contact?->getName();\n }\n\n /**\n * Track if a participant was added or updated to determine if CRM lookup is needed.\n * Sets $this->participantsChanged to true if the participant is new or has relevant attribute changes.\n * Timestamp fields (created_at, updated_at, deleted_at) are excluded from change detection.\n */\n private function trackParticipantChanges(Participant $participant, string $email): void\n {\n $relevantChanges = array_diff_key(\n $participant->getChanges() ?? [],\n array_flip(['created_at', 'updated_at', 'deleted_at'])\n );\n\n if ($participant->wasRecentlyCreated) {\n $this->participantsChanged = true;\n\n Log::info('[Calendar] New participant added', [\n 'activity_id' => $this->activity->getUuid(),\n 'email' => $email,\n 'participant_id' => $participant->getId(),\n ]);\n\n return;\n }\n\n if (! empty($relevantChanges)) {\n $this->participantsChanged = true;\n\n Log::info('[Calendar] Participant updated', [\n 'activity_id' => $this->activity->getUuid(),\n 'email' => $email,\n 'participant_id' => $participant->getId(),\n 'changes' => $relevantChanges,\n ]);\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.6665558,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"169","depth":4,"bounds":{"left":0.6765292,"top":0.12529927,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.69049203,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"102","depth":4,"bounds":{"left":0.70046544,"top":0.12529927,"width":0.011968086,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.7144282,"top":0.12529927,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.7240692,"top":0.123703115,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.73138297,"top":0.123703115,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Models;\n\nuse Carbon\\Carbon;\nuse Database\\Factories\\ActivityFactory;\nuse DateTimeInterface;\nuse Exception;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse Illuminate\\Database\\Eloquent;\nuse Illuminate\\Database\\Eloquent\\Attributes\\Scope;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\HasFactory;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne;\nuse Illuminate\\Database\\Eloquent\\SoftDeletes;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Auth;\nuse InvalidArgumentException;\nuse Jiminny\\Component\\ElasticSearch;\nuse Jiminny\\Component\\MeetingBot;\nuse Jiminny\\Component\\Model\\BitwiseFlagTrait;\nuse Jiminny\\Component\\PlaybackPage\\Comments\\Services\\ActivityCommentService;\nuse Jiminny\\Component\\Sidekick\\SidekickService;\nuse Jiminny\\Component\\Uuid\\UuidAwareInterface;\nuse Jiminny\\Component\\Workflow;\nuse Jiminny\\Contracts;\nuse Jiminny\\Contracts\\Crm\\ProspectInterface;\nuse Jiminny\\DTO\\ImportCall\\Call;\nuse Jiminny\\Events\\Activities\\ActivityTypeUpdated;\nuse Jiminny\\Events\\Activities\\ActivityUpdated;\nuse Jiminny\\Events\\Activities\\ProspectUpdated;\nuse Jiminny\\Events\\Activities\\StageUpdated;\nuse Jiminny\\Events\\Activities\\StatusUpdated;\nuse Jiminny\\Events\\Activities\\TitleUpdated;\nuse Jiminny\\Exceptions\\InvalidArgumentException as InvalidArgumentJiminnyException;\nuse Jiminny\\Exceptions\\LogicException;\nuse Jiminny\\Exceptions\\RuntimeException;\nuse Jiminny\\Models;\nuse Jiminny\\Models\\Activity\\ActivitySummaryLog;\nuse Jiminny\\Models\\Activity\\ActivityUploadSetting;\nuse Jiminny\\Models\\Activity\\AvailabilityNotification;\nuse Jiminny\\Models\\Activity\\CoachRequest;\nuse Jiminny\\Models\\Activity\\Comment;\nuse Jiminny\\Models\\Activity\\Log;\nuse Jiminny\\Models\\Activity\\Message;\nuse Jiminny\\Models\\Activity\\Moment;\nuse Jiminny\\Models\\Activity\\Note;\nuse Jiminny\\Models\\Activity\\ParticipantSpeech;\nuse Jiminny\\Models\\Activity\\Play;\nuse Jiminny\\Models\\Activity\\Question;\nuse Jiminny\\Models\\Activity\\Share;\nuse Jiminny\\Models\\Activity\\Snapshot;\nuse Jiminny\\Models\\Activity\\Stats;\nuse Jiminny\\Models\\Activity\\SubscriptionSet;\nuse Jiminny\\Models\\Activity\\TopicTrigger;\nuse Jiminny\\Models\\Activity\\Transcription;\nuse Jiminny\\Models\\Calendar\\CalendarEvent;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\ElasticSearch\\ActivityElasticSearchTrait;\nuse Jiminny\\Models\\Feature\\FeatureEnum;\nuse Jiminny\\Models\\Participant\\Connection;\nuse Jiminny\\Models\\Playlist\\Activity as PlaylistActivity;\nuse Jiminny\\Services\\Activity\\ActivityProviderRegistry;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataByStrategy;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverFactory;\nuse Jiminny\\Services\\Activity\\Import\\DataResolvers\\UpdateCrmDataResolverInterface;\nuse Jiminny\\Traits\\Enums;\nuse Jiminny\\Traits\\RequiresUUID;\nuse Jiminny\\Utils\\CurrencyFormatter;\nuse NumberFormatter;\n\nuse function in_array;\n\n/**\n * Jiminny\\Models\\Activity\n *\n * @property null|int $auto_score filled from ES hydrator, not in DB!\n * @property-read Account|null $account\n * @property-read CalendarEvent|null $calendarEvent\n * @property-read Contact|null $contact\n * @property-read Lead|null $lead\n * @property-read Opportunity|null $opportunity\n * @property-read Stage|null $stage\n * @property int $id\n * @property mixed|null $uuid\n * @property string|null $source\n * @property string|null $external_id\n * @property string $provider\n * @property string|null $location\n * @property string|null $telephony_provider_id\n * @property int|null $from_participant_id\n * @property int|null $to_participant_id\n * @property int|null $device_id\n * @property string|null $type\n * @property int|null $playbook_category_id\n * @property int $user_id\n * @property int|null $lead_id\n * @property int|null $account_id\n * @property int|null $contact_id\n * @property int|null $opportunity_id\n * @property int|null $stage_id\n * @property string|null $value\n * @property int|null $crm_configuration_id\n * @property string|null $crm_provider_id\n * @property string|null $language\n * @property int|null $transcription_id\n * @property int $duration\n * @property string $status\n * @property int|null $on_air\n * @property int|null $calendar_event_id\n * @property string $recording_state\n * @property bool|null $recording_preference\n * @property int $recording_reason_code\n * @property int $summary_reminder_sent\n * @property \\Illuminate\\Support\\Carbon|null $log_reminder_sent_at\n * @property \\Illuminate\\Support\\Carbon|null $organizer_notified_at\n * @property bool|null $has_recording_prompt\n * @property bool $is_internal\n * @property int $is_locked\n * @property int $is_recording\n * @property bool|null $is_processed\n * @property bool $is_private\n * @property bool $is_instant_invite\n * @property string|null $poster_path\n * @property string|null $summary\n * @property string|null $title\n * @property string|null $description\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_start_time\n * @property \\Illuminate\\Support\\Carbon|null $scheduled_end_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_start_time\n * @property \\Illuminate\\Support\\Carbon|null $actual_end_time\n * @property int|null $uploaded_by\n * @property \\Illuminate\\Support\\Carbon|null $deleted_at\n * @property \\Illuminate\\Support\\Carbon|null $created_at\n * @property \\Illuminate\\Support\\Carbon|null $updated_at\n * @property string|null $average_score\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $activeParticipants\n * @property-read int|null $active_participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers\n * @property-read int|null $activity_scorecard_rule_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Scorecard\\ActivityScorecardRule> $activityScorecardRules\n * @property-read int|null $activity_scorecard_rules_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, AvailabilityNotification> $availabilityNotifications\n * @property-read int|null $availability_notifications_count\n * @property-read \\Jiminny\\Models\\PlaybookCategory|null $category\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, CoachRequest> $coachRequests\n * @property-read int|null $coach_requests_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $coachingFeedbacks\n * @property-read int|null $coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $coachingMessages\n * @property-read int|null $coaching_messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $comments\n * @property-read int|null $comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Connection> $connections\n * @property-read int|null $connections_count\n * @property-read Configuration|null $crm\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, FieldData> $data\n * @property-read int|null $data_count\n * @property-read \\Jiminny\\Models\\Device|null $device\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $favoritePlaylists\n * @property-read int|null $favorite_playlists_count\n * @property-read \\Jiminny\\Models\\Participant|null $from\n * @property-read string|null $activity_title\n * @property-read mixed $comment_count\n * @property-read mixed $duration_for_humans\n * @property-read string $duration_for_humans_short\n * @property-read int $favorite_count\n * @property-read mixed $favorites_count\n * @property-read mixed $formatted_value\n * @property-read string $id_string\n * @property-read \\Jiminny\\Models\\Participant|null $organizer\n * @property-read mixed $play_count\n * @property-read int|null $plays_count\n * @property-read ?ProspectInterface $prospect\n * @property-read string|null $prospect_name\n * @property-read mixed $prospect_type\n * @property-read mixed $share_count\n * @property-read int|null $shares_count\n * @property-read int|null $tracks_with_telephony_count\n * @property-read int|null $visible_comments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\CoachingFeedback> $latestCoachingFeedbacks\n * @property-read int|null $latest_coaching_feedbacks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Log> $logs\n * @property-read int|null $logs_count\n * @property-read \\Jiminny\\Models\\Track|null $masterTrack\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Message> $messages\n * @property-read int|null $messages_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Moment> $moments\n * @property-read int|null $moments_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Note> $notes\n * @property-read int|null $notes_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\Share> $participantShares\n * @property-read int|null $participant_shares_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, ParticipantSpeech> $participantSpeeches\n * @property-read int|null $participant_speeches_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant\\ParticipantStats> $participantStats\n * @property-read int|null $participant_stats_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Participant> $participants\n * @property-read int|null $participants_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, PlaylistActivity> $playlistActivities\n * @property-read int|null $playlist_activities_count\n * @property-read \\Kalnoy\\Nestedset\\Collection<int, \\Jiminny\\Models\\Playlist> $playlists\n * @property-read int|null $playlists_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Play> $plays\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Question> $questions\n * @property-read int|null $questions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Session> $sessions\n * @property-read int|null $sessions_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Share> $shares\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Snapshot> $snapshots\n * @property-read int|null $snapshots_count\n * @property-read Stats|null $stats\n * @property-read \\Jiminny\\Models\\Participant|null $to\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, TopicTrigger> $topicTriggers\n * @property-read int|null $topic_triggers_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracks\n * @property-read int|null $tracks_count\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, \\Jiminny\\Models\\Track> $tracksWithTelephony\n * @property-read Transcription|null $transcription\n * @property-read \\Jiminny\\Models\\User $user\n * @property-read \\Illuminate\\Database\\Eloquent\\Collection<int, Comment> $visibleComments\n *\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> all($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)\n * @method static \\Database\\Factories\\ActivityFactory factory(...$parameters)\n * @method static \\Illuminate\\Database\\Eloquent\\Collection<int, static> get($columns = ['*'])\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity heldBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity idOrUuId($idOrUuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newModelQuery()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity newQuery()\n * @method static Builder|Activity onlyTrashed()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity query()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity scheduledBetween(\\Carbon\\Carbon $start, \\Carbon\\Carbon $end)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity inOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity notInOpenDeals()\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity forTeam(int $teamId)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity uuid(string $uuid, bool $first = true)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAccountId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereActualStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereAverageScore($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCalendarEventId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereContactId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCreatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmConfigurationId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereCrmProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeletedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDescription($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDeviceId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereDuration($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereFromParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereHasRecordingPrompt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInstantInvite($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsInternal($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsLocked($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsPrivate($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsProcessed($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereIsRecording($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLanguage($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLeadId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLocation($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereLogReminderSentAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOnAir($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOpportunityId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereOrganizerNotifiedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePlaybookCategoryId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity wherePosterPath($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereProvider($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingPreference($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingReasonCode($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereRecordingState($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledEndTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereScheduledStartTime($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSource($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereExternalId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStageId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereStatus($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummary($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereSummaryReminderSent($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTelephonyProviderId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTitle($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereToParticipantId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereTranscriptionId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereType($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUpdatedAt($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUploadedBy($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUserId($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereUuid($value)\n * @method static \\Jiminny\\Component\\Eloquent\\Builder|Activity whereValue($value)\n * @method static Builder|Activity withTrashed()\n * @method static Builder|Activity withoutTrashed()\n *\n * @mixin \\Eloquent\n */\nclass Activity extends Model implements\n ElasticSearch\\Contract\\Searchable,\n Workflow\\Workflow\\WorkflowAwareInterface,\n Models\\Contracts\\ActivityContract,\n Contracts\\Model\\ActivityInterface,\n UuidAwareInterface\n{\n use HasFactory;\n\n use Enums;\n use SoftDeletes;\n use RequiresUUID;\n use BitwiseFlagTrait;\n use ElasticSearch\\Model\\Searchable;\n use ActivityElasticSearchTrait;\n\n use Workflow\\Workflow\\WorkflowAware {\n transitionTo as traitTransitionTo;\n }\n\n public const int FLAG_RECORDING_REASON_DEFAULT = 0;\n\n // Recording Prompted but never started\n public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;\n public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;\n\n // Recording Disabled by Organization\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;\n\n // Recording was restricted to one-side recordings only\n public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;\n\n // Recording was not started because it was internal and team setting disabled that.\n public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;\n\n // Recording was not started because it was internal and user setting disabled that.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;\n\n // Recording was not started because user setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;\n\n // Recording was not started because team setting disabled automatic recording.\n public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;\n\n // Recording was not started because user has overriden default.\n public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;\n\n // Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.\n public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;\n\n // Recording was not started because their team setting does excludes the meeting type.\n public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;\n\n // Recording was not started because the external provider disabled it (or recording is missing etc).\n public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;\n\n // Recording was stopped externally (\"exit-meeting\" Pusher event)\n public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;\n\n // Recording couldn't be started due to Zoom hosting conflict error\n public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;\n\n // meeting.failed event with reason code BOT_DENIED_FROM_LOBBY\n public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;\n\n // meeting.failed event with reason code LOBBY_TIMEOUT\n public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;\n\n // meeting.failed event with reason code BOT_KICKED\n public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;\n\n // meeting.failed event with reason code UNKNOWN\n public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;\n\n public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;\n\n // Invalid meeting (e.g. URL is invalid, or the meeting is not found)\n public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;\n\n // The host stopped the recording.\n public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;\n\n // Recording was not started because an alternative vendor disabled it (or overrode it).\n public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;\n\n // Login required meeting.failed code\n public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;\n\n // Password for meeting was not provided - meeting.failed code\n public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;\n\n // meeting.failed - when the meeting is locked\n public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;\n\n // max recording duration reached\n public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;\n\n // recording size is too small\n public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;\n\n // meeting.failed - when bot is redirected to sign in page multiple times\n public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;\n\n // meeting.failed event with reason code CONNECTION_LOST\n public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;\n\n // recording is corrupted.\n public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;\n\n // meeting ended in lobby\n public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;\n\n // meeting not started\n public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;\n\n // unfinished zoom custom disclaimer\n public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;\n\n // recording download failed - server error\n public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;\n\n // recording download failed - client code 404\n public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;\n\n // recording download failed - client code 401, 403\n public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;\n\n // recording download failed - client code 429\n public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;\n\n // recording download failed - unknown client error\n public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;\n\n // recording download failed - unknown error\n public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;\n\n // It has been setup ahead of time through calendar\n public const string STATUS_SCHEDULED = 'scheduled';\n\n // It is awaiting audio.\n public const string STATUS_PENDING = 'pending';\n\n // Participant(s) dialed in, awaiting organizer.\n public const string STATUS_RINGING = 'ringing';\n\n // Call is in progress.\n public const string STATUS_IN_PROGRESS = 'in-progress';\n\n // It has ended.\n public const string STATUS_COMPLETED = 'completed';\n\n // Cancelled prior to starting.\n public const string STATUS_CANCELLED = 'canceled';\n\n public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference\n\n public const string STATUS_STARTING_SOON = 'starting-soon';\n\n public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';\n\n public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';\n\n public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';\n\n // When bot instance is waiting in lobby\n public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';\n\n public const string STATUS_BUSY = 'busy';\n public const string STATUS_NO_ANSWER = 'no-answer';\n public const string STATUS_FAILED = 'failed'; // Used by SMS too\n\n // SMS related\n public const string STATUS_ACCEPTED = 'accepted';\n public const string STATUS_QUEUED = 'queued';\n public const string STATUS_SENDING = 'sending';\n public const string STATUS_SENT = 'sent';\n public const string STATUS_DELIVERED = 'delivered';\n public const string STATUS_UNDELIVERED = 'undelivered';\n public const string STATUS_RECEIVING = 'receiving';\n public const string STATUS_RECEIVED = 'received';\n public const string STATUS_RESENT = 'resent';\n\n public const array SMS_STATUSES = [\n Activity::STATUS_RECEIVED,\n Activity::STATUS_SENT,\n Activity::STATUS_DELIVERED,\n ];\n\n public const array SOFT_PHONE_CONFERENCE_STATUSES = [\n Activity::STATUS_IN_PROGRESS,\n Activity::STATUS_COMPLETED,\n ];\n\n // @todo refactor prefix from `TYPE_` to `CHANNEL_`\n public const string TYPE_SOFTPHONE = 'softphone';\n public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';\n public const string TYPE_CONFERENCE = 'conference';\n public const string TYPE_SMS_INBOUND = 'sms-inbound';\n public const string TYPE_SMS_OUTBOUND = 'sms-outbound';\n public const string TYPE_EMAIL_INBOUND = 'email-inbound';\n public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';\n\n public const array CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n public const array PLAYABLE_CHANNELS = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n ];\n\n // Recording States\n public const string RECORDING_OFF = 'off'; // Default state\n public const string RECORDING_IN_PROGRESS = 'in-progress';\n public const string RECORDING_PAUSED = 'paused';\n public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.\n public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.\n public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.\n\n // Live Stream States\n public const int ON_AIR_DEFAULT = 0;\n public const int ON_AIR_READY = 1;\n public const int ON_AIR_PREPARING = 2;\n public const int ON_AIR_STREAMING = 3;\n public const int ON_AIR_FINISHED = 4;\n public const int ON_AIR_NOT_STREAMED = 5;\n public const int ON_AIR_ERROR = -1;\n\n public const string SOURCE_GONG = 'gong';\n public const string SOURCE_CHORUS = 'chorus';\n public const string SOURCE_OUTLOOK = 'outlook';\n public const string SOURCE_GOOGLE = 'google';\n\n // Activity Providers\n public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.\n public const string PROVIDER_OUTREACH = 'outreach';\n public const string PROVIDER_ZOOM_BOT = 'zoom-bot';\n public const string PROVIDER_SALESLOFT = 'salesloft';\n public const string PROVIDER_GOOGLE = 'google';\n public const string PROVIDER_AIRCALL = 'aircall';\n public const string PROVIDER_JUSTCALL = 'justcall';\n public const string PROVIDER_GOOGLE_MEET = 'google-meet';\n public const string PROVIDER_GONG = 'gong';\n public const string PROVIDER_HUBSPOT = 'hubspot';\n public const string PROVIDER_CLOSE = 'close';\n public const string PROVIDER_TEAMS = 'ms-teams';\n public const string PROVIDER_SALESFORCE = 'salesforce';\n public const string PROVIDER_GROOVE = 'groove';\n public const string PROVIDER_XANT = 'xant';\n public const string PROVIDER_OFFICE = 'office';\n public const string PROVIDER_NATTERBOX = 'natterbox';\n public const string PROVIDER_RINGCENTRAL = 'ringcentral';\n public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';\n public const string PROVIDER_GOTOMEETING = 'go-to-meeting';\n public const string PROVIDER_DEMODESK = 'demo-desk';\n public const string PROVIDER_DIALPAD = 'dialpad';\n public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';\n public const string PROVIDER_CLOUDCALL = 'cloudcall';\n public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';\n public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // \"8x8\" UK\n public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // \"8x8\" Canada\n public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // \"8x8\" Australia\n public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // \"8x8\" US East\n public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // \"8x8\" US West\n public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';\n public const string PROVIDER_CLOUD_TALK = 'cloud-talk';\n public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';\n public const string PROVIDER_VONAGE = 'vonage';\n public const string PROVIDER_MIGRATOR = 'migrator';\n public const string PROVIDER_UPLOADER = 'uploader';\n public const string PROVIDER_TALKDESK = 'talkdesk';\n public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';\n public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';\n public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';\n public const string PROVIDER_AVAYA = 'avaya';\n public const string PROVIDER_TELUS = 'telus';\n public const string PROVIDER_FIVE_NINE = 'five-nine';\n public const string PROVIDER_APOLLO = 'apollo';\n public const string PROVIDER_ORUM = 'orum';\n public const string PROVIDER_BLOOBIRDS = 'bloobirds';\n\n /**\n * @const API_PROVIDERS\n * A list of integrations that import calls via API instead of webhooks\n */\n public const array API_PROVIDERS = [\n self::PROVIDER_OUTREACH,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n ];\n\n public const array FINITE_STATES = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_NO_ANSWER,\n self::STATUS_BUSY,\n ],\n self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,\n ];\n\n public const array FINITE_STATES_CONFERENCE = [\n self::STATUS_COMPLETED,\n self::STATUS_FAILED,\n self::STATUS_CANCELLED,\n ];\n\n public const array MEETING_BOT_JOIN_ATTEMPTED = [\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_BOT_INSTANCE_STARTED,\n ];\n\n public static array $enumStatuses = [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_ACCEPTED,\n self::STATUS_QUEUED,\n self::STATUS_SENDING,\n self::STATUS_SENT,\n self::STATUS_RESENT,\n self::STATUS_DELIVERED,\n self::STATUS_UNDELIVERED,\n self::STATUS_RECEIVING,\n self::STATUS_RECEIVED,\n self::STATUS_BOT_INSTANCE_WAITING_LOBBY,\n self::STATUS_STARTING_SOON,\n self::STATUS_BOT_INSTANCE_WORKER_ASSIGNED,\n self::STATUS_BOT_INSTANCE_STARTED,\n self::STATUS_DUPLICATED,\n ];\n\n public static array $enumProviders = [\n self::PROVIDER_TWILIO,\n self::PROVIDER_OUTREACH,\n self::PROVIDER_ZOOM_BOT,\n self::PROVIDER_SALESLOFT,\n self::PROVIDER_AIRCALL,\n self::PROVIDER_JUSTCALL,\n self::PROVIDER_GOOGLE_MEET,\n self::PROVIDER_GONG,\n self::PROVIDER_HUBSPOT,\n self::PROVIDER_CLOSE,\n self::PROVIDER_TEAMS,\n self::PROVIDER_SALESFORCE,\n self::PROVIDER_GROOVE,\n self::PROVIDER_XANT,\n self::PROVIDER_GOOGLE,\n self::PROVIDER_OFFICE,\n self::PROVIDER_NATTERBOX,\n self::PROVIDER_RINGCENTRAL,\n self::PROVIDER_RINGCENTRAL_VIDEO,\n self::PROVIDER_GOTOMEETING,\n self::PROVIDER_DEMODESK,\n self::PROVIDER_DIALPAD,\n self::PROVIDER_ZOOM_PHONE,\n self::PROVIDER_CLOUDCALL,\n self::PROVIDER_CLOUDCALL_US,\n self::PROVIDER_EIGHT_BY_EIGHT,\n self::PROVIDER_EIGHT_BY_EIGHT_CA,\n self::PROVIDER_EIGHT_BY_EIGHT_AP,\n self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,\n self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,\n self::PROVIDER_CONNECT_AND_SELL,\n self::PROVIDER_CLOUD_TALK,\n self::PROVIDER_AMAZON_CONNECT,\n self::PROVIDER_VONAGE,\n self::PROVIDER_TALKDESK,\n self::PROVIDER_TWILIO_FLEX,\n self::PROVIDER_TWILIO_FLEX_DIRECT,\n self::PROVIDER_TWILIO_VIDEO,\n self::PROVIDER_AVAYA,\n self::PROVIDER_TELUS,\n self::PROVIDER_FIVE_NINE,\n self::PROVIDER_APOLLO,\n self::PROVIDER_ORUM,\n self::PROVIDER_BLOOBIRDS,\n ];\n\n public static $enumRecordingStates = [\n self::RECORDING_OFF, // Default state\n self::RECORDING_IN_PROGRESS,\n self::RECORDING_PAUSED,\n self::RECORDING_STOPPED,\n self::RECORDING_RECORDED,\n self::RECORDING_FAILED,\n ];\n\n // @Important:\n // This collection is not used anywhere, and is fully duplicated by the Channels const.\n // Validate if it is referred somehow via the enum trait, and if not, remove it entirely.\n // An even better strategy will be to move all those constants to a dedicated class\n protected array $enumTypes = [\n self::TYPE_SOFTPHONE,\n self::TYPE_SOFTPHONE_INBOUND,\n self::TYPE_CONFERENCE,\n self::TYPE_SMS_INBOUND,\n self::TYPE_SMS_OUTBOUND,\n self::TYPE_EMAIL_INBOUND,\n self::TYPE_EMAIL_OUTBOUND,\n ];\n\n protected static $enumFailedStatuses = [\n self::STATUS_NO_ANSWER,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n self::STATUS_CANCELLED,\n ];\n\n protected $table = 'activities';\n\n protected $fillable = [\n // Type of activity.\n 'type', // @todo refactor to `channel`\n // The activity type.\n 'playbook_category_id',\n // User who hosts the activity.\n 'user_id',\n // Related Lead record (if applicable)\n 'lead_id',\n // Related Account record (if applicable)\n 'account_id',\n // Related Contact record (if applicable)\n 'contact_id',\n // Related Opportunity record (if applicable)\n 'opportunity_id',\n // Stage of activity.\n 'stage_id',\n // Value of opportunity.\n 'value',\n // If the activity relates to a CRM task.\n 'crm_provider_id',\n // If the activity was created through an external device.\n 'device_id',\n // the activity's language code\n 'language',\n // transcription id\n 'transcription_id',\n // Duration of the call, with microseconds precision.\n 'duration',\n // One of enumStatuses above.\n 'status',\n // Have we reminded them to log the call?\n 'log_reminder_sent_at',\n // If activity is private or inter-org, flagged here.\n 'is_internal',\n // Managers and above can mark a call as private, to exclude it from other team members\n 'is_private',\n 'is_processed',\n // Boolean for this activity being instant invite handled.\n 'is_instant_invite',\n // If activity is in recording state, flagged here.\n 'recording_state',\n // If activity recording is overidden from default.\n 'recording_preference',\n // if recording did (not) happen, why that is\n 'recording_reason_code',\n // Average score, updated during\n 'average_score',\n // Summary that the organizer has taken after the call.\n 'summary',\n // Subject of the activity, usually taken from calendar event.\n 'title',\n // Description of the activity, usually taken from calendar event.\n 'description',\n // Start time, usually taken from calendar event.\n 'scheduled_start_time',\n // End time, usually taken from calendar event.\n 'scheduled_end_time',\n // When the call actually started.\n 'actual_start_time',\n // When the call actually ended.\n 'actual_end_time',\n // SMS: Message reference\n 'telephony_provider_id',\n // SMS: Participant who sent message\n 'from_participant_id',\n // SMS: Participant who should receive the message\n 'to_participant_id',\n // When an external guest joins an organizers meeting room and the organizer is not present,\n // send them an SMS notification that someone has joined.\n 'organizer_notified_at',\n // where was the activity imported from\n 'source',\n // The id in the source system (e.g. the bot id in Recall.ai)\n 'external_id',\n // The provider, by default it is twilio.\n 'provider',\n // Meeting location url\n 'location',\n // The snapshot for displaying a poster image.\n 'poster_path',\n 'crm_configuration_id',\n // If there is an automated message that the conversation is being recorded\n 'has_recording_prompt',\n // If the activity is being live-streamed\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected $appends = [\n 'id_string',\n 'organizer',\n ];\n\n protected $hidden = [\n 'uuid',\n ];\n\n protected $visible = [\n 'id_string',\n 'type',\n 'duration',\n 'average_score',\n 'status',\n 'log_reminder_sent_at',\n 'title',\n 'description',\n 'is_internal',\n 'scheduled_start_time',\n 'scheduled_end_time',\n 'actual_start_time',\n 'actual_end_time',\n 'user',\n 'category',\n 'account',\n 'contact',\n 'opportunity',\n 'lead',\n 'stage',\n 'stats',\n 'participants',\n 'playlists',\n 'tracks',\n 'comments',\n 'plays',\n 'coachingFeedbacks',\n 'shares',\n 'favorites',\n 'language',\n 'transcription',\n 'is_private',\n 'is_instant_invite',\n 'on_air',\n 'calendar_event_id',\n ];\n\n protected function casts(): array\n {\n return [\n 'scheduled_start_time' => 'datetime',\n 'scheduled_end_time' => 'datetime',\n 'actual_start_time' => 'datetime',\n 'actual_end_time' => 'datetime',\n 'organizer_notified_at' => 'datetime',\n 'log_reminder_sent_at' => 'datetime',\n 'is_internal' => 'boolean',\n 'duration' => 'integer',\n 'average_score' => 'decimal:2',\n 'is_private' => 'boolean',\n 'is_processed' => 'boolean',\n 'is_instant_invite' => 'boolean',\n 'value' => 'decimal:2',\n 'recording_preference' => 'boolean',\n 'recording_reason_code' => 'integer',\n 'has_recording_prompt' => 'boolean',\n 'on_air' => 'integer',\n ];\n }\n\n protected static function boot()\n {\n parent::boot();\n\n static::updated(static function (Activity $activity) {\n // If activity is about to start (pending, ringing, in-progress) or event is scheduled in less than 1 week\n if (in_array($activity->status, [Activity::STATUS_PENDING, Activity::STATUS_RINGING, Activity::STATUS_IN_PROGRESS], true) ||\n ($activity->scheduled_start_time && (int) $activity->scheduled_start_time->diffInWeeks(new Carbon(), true) < 1)) {\n if ($activity->isDirty('status')) {\n event(new StatusUpdated($activity));\n }\n\n if ($activity->isDirty('stage_id')) {\n event(new StageUpdated($activity));\n }\n\n if ($activity->isDirty(['lead_id', 'account_id', 'contact_id'])) {\n event(new ProspectUpdated($activity));\n }\n\n if ($activity->isDirty('opportunity_id')) {\n event(new ActivityUpdated($activity, 'activity.opportunity-updated', Auth::user()));\n }\n\n if ($activity->isDirty('title')) {\n event(new TitleUpdated($activity));\n }\n }\n\n if ($activity->isDirty('playbook_category_id')) {\n event(new ActivityTypeUpdated($activity));\n }\n });\n\n static::deleted(static function (Activity $activity) {\n // Hard delete associated playlistActivities\n $activity->playlistActivities()->delete();\n });\n }\n\n public function getOrganizerAttribute(): ?Participant\n {\n $participant = $this->participants()->where('user_id', $this->user_id)->first();\n\n if (! $participant instanceof Participant && $participant !== null) {\n throw new RuntimeException(sprintf('$participant must be an instance of \"%s\" or null', Participant::class));\n }\n\n return $participant;\n }\n\n public function getFormattedValueAttribute()\n {\n $currencyCode = 'USD';\n if ($this->opportunity) {\n $currencyCode = $this->opportunity->getCurrencyCode();\n }\n\n $formatter = new CurrencyFormatter();\n $formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, $currencyCode);\n $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);\n\n return $formatter->format($this->value, $currencyCode);\n }\n\n public function getProspectNameAttribute(): ?string\n {\n $prospectName = null;\n\n if ($this->lead_id) {\n $prospectName = $this->lead->name;\n } elseif ($this->contact_id) {\n $prospectName = $this->contact->name;\n } elseif ($this->account_id) {\n $prospectName = $this->account->name;\n }\n\n return $prospectName;\n }\n\n public function getProspectName(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('prospect_name');\n }\n\n /**\n * Get activity title depending on prospect or title\n */\n public function getActivityTitleAttribute(): ?string\n {\n $activityTitle = null;\n if ($this->prospect && $this->prospect->getName()) {\n if ($this->account_id) {\n $activityTitle = $this->account->name;\n } elseif ($this->lead_id) {\n $activityTitle = $this->lead->company;\n } elseif ($this->contact_id) {\n $activityTitle = $this->contact->account ? $this->contact->account->name : $this->contact->name;\n }\n } elseif ($this->title) {\n $activityTitle = $this->title;\n }\n\n return $activityTitle;\n }\n\n public function wasRecentlyCreated(): bool\n {\n return $this->wasRecentlyCreated;\n }\n\n public function getProspectTypeAttribute()\n {\n $prospectType = null;\n\n if ($this->lead_id) {\n $prospectType = 'Lead';\n } elseif ($this->contact_id) {\n $prospectType = 'Contact';\n } elseif ($this->account_id) {\n $prospectType = 'Account';\n }\n\n return $prospectType;\n }\n\n /**\n * Return the best match for prospect. Results are in the following order of priority:\n * 1. Lead\n * 2. Contact\n * 3. Account\n * 4. NULL\n */\n public function getProspectAttribute(): ?ProspectInterface\n {\n if ($this->hasLead()) {\n return $this->getLead();\n }\n\n if ($this->hasContact()) {\n return $this->getContact();\n }\n\n if ($this->hasAccount()) {\n return $this->getAccount();\n }\n\n return null;\n }\n\n public function getTitleAttribute($value): ?string\n {\n return \\getActivityTitleAttribute(\n $this->user->name,\n $this->getType(),\n $value,\n $this->prospect->name ?? null,\n $this->from->national_phone_number ?? null\n );\n }\n\n public function getTitle(): ?string\n {\n return $this->getAttribute('title');\n }\n\n public function getSummary(): ?string\n {\n return $this->getAttribute('summary');\n }\n\n public function isInternal(): bool\n {\n return $this->getAttribute('is_internal');\n }\n\n public function getIsPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n public function getDescription(): ?string\n {\n return $this->getAttribute('description');\n }\n\n public function hasTitle(): bool\n {\n return $this->getOriginal('title') !== null;\n }\n\n public function getPlayCountAttribute()\n {\n return $this->getPlaysCountAttribute();\n }\n\n public function getPlaysCountAttribute()\n {\n if (! isset($this->attributes['plays_count'])) {\n $this->loadCount('plays');\n }\n\n return $this->attributes['plays_count'];\n }\n\n public function getCommentCountAttribute()\n {\n return $this->getCommentsCountAttribute();\n }\n\n public function getCommentsCountAttribute()\n {\n if (! isset($this->attributes['comments_count'])) {\n $this->loadCount('comments');\n }\n\n return $this->attributes['comments_count'];\n }\n\n public function getVisibleCommentsCountAttribute()\n {\n if (! isset($this->attributes['visible_comments_count'])) {\n $activityCommentsService = app(ActivityCommentService::class);\n $user = Auth::user() instanceof User ? Auth::user() : null;\n $this->attributes['visible_comments_count'] = $activityCommentsService\n ->getVisibleCommentsCount($this, $user);\n }\n\n return $this->attributes['visible_comments_count'];\n }\n\n public function getShareCountAttribute()\n {\n return $this->getSharesCountAttribute();\n }\n\n public function getSharesCountAttribute()\n {\n if (! isset($this->attributes['shares_count'])) {\n $this->loadCount('shares');\n }\n\n return $this->attributes['shares_count'];\n }\n\n\n /**\n * Get the count of favorites playlists this activity appears in\n */\n public function getFavoriteCountAttribute(): int\n {\n return $this->getFavoritesCountAttribute();\n }\n\n public function getFavoritesCountAttribute()\n {\n if (! isset($this->attributes['favorites_count'])) {\n $this->loadCount('favorites');\n }\n\n return $this->attributes['favorites_count'];\n }\n\n public function getActiveParticipantsCountAttribute()\n {\n if (! isset($this->attributes['active_participants_count'])) {\n $this->loadCount('activeParticipants');\n }\n\n return $this->attributes['active_participants_count'];\n }\n\n public function getTracksWithTelephonyCountAttribute()\n {\n if (! isset($this->attributes['tracks_with_telephony_count'])) {\n $this->loadCount('tracksWithTelephony');\n }\n\n return $this->attributes['tracks_with_telephony_count'];\n }\n\n /**\n * @TEMP\n * $this->loadCount('tracksWithTelephony') throws null pointer exception\n */\n public function countTracksWithTelephony(): int\n {\n return $this->tracks()->whereNotNull('telephony_provider_id')->count();\n }\n\n public function getDuration(): float\n {\n return $this->getAttribute('duration');\n }\n\n public function getDurationForHumansAttribute()\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true);\n }\n\n public function getDurationForHumansShortAttribute(): string\n {\n return Carbon::now()->subSeconds($this->duration)->diffForHumans(now(), true, true);\n }\n\n public function hasRecordingPreference(): bool\n {\n return $this->getAttribute('recording_preference') !== null;\n }\n\n public function getRecordingPreference()\n {\n return $this->getAttribute('recording_preference');\n }\n\n /** @return BelongsTo<User, self> */\n public function user(): BelongsTo\n {\n return $this->belongsTo(User::class)->with('group');\n }\n\n public function device()\n {\n return $this->belongsTo(Device::class);\n }\n\n public function category()\n {\n return $this->belongsTo(PlaybookCategory::class, 'playbook_category_id');\n }\n\n public function getCategory(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function getPlaybookCategoryId(): ?int\n {\n return $this->getAttribute('playbook_category_id');\n }\n\n public function hasStats(): bool\n {\n return $this->getAttribute('stats') !== null;\n }\n\n public function getStats(): ?Stats\n {\n return $this->getAttribute('stats');\n }\n\n public function stats(): HasOne\n {\n return $this->hasOne(Stats::class);\n }\n\n public function participantStats(): Eloquent\\Relations\\HasManyThrough\n {\n return $this->hasManyThrough(\n Models\\Participant\\ParticipantStats::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantStats(): Eloquent\\Collection\n {\n return $this->getAttribute('participantStats');\n }\n\n public function account()\n {\n return $this->belongsTo(Account::class);\n }\n\n public function contact()\n {\n return $this->belongsTo(Contact::class)->with(['account']);\n }\n\n public function lead()\n {\n return $this->belongsTo(Lead::class)->with(['stage', 'recordType']);\n }\n\n /**\n * @return BelongsTo<Opportunity, self>\n */\n public function opportunity(): BelongsTo\n {\n /** @var BelongsTo<Opportunity, self> */\n return $this->belongsTo(Opportunity::class);\n }\n\n public function stage()\n {\n return $this->belongsTo(Stage::class);\n }\n\n /**\n * @return HasMany<Session>\n */\n public function sessions(): HasMany\n {\n return $this->hasMany(Session::class);\n }\n\n /**\n * @return HasMany|ParticipantSpeech[]|Eloquent\\Collection\n */\n public function participantSpeeches()\n {\n return $this->hasMany(ParticipantSpeech::class);\n }\n\n public function getParticipantSpeeches(): Eloquent\\Collection\n {\n return $this->getAttribute('participantSpeeches');\n }\n\n /**\n * @return HasMany|Log[]|Eloquent\\Collection\n */\n public function logs()\n {\n return $this->hasMany(Log::class);\n }\n\n /**\n * @return HasMany|Moment[]|Eloquent\\Collection\n */\n public function moments()\n {\n return $this->hasMany(Moment::class);\n }\n\n /**\n * @return HasMany|Note[]|Eloquent\\Collection\n */\n public function notes()\n {\n return $this->hasMany(Note::class);\n }\n\n /**\n * @return Eloquent\\Collection|Note[]\n */\n public function getNotes(): Eloquent\\Collection\n {\n return $this->getAttribute('notes');\n }\n\n /**\n * @return HasMany|Message[]|Eloquent\\Collection\n */\n public function messages()\n {\n return $this->hasMany(Message::class);\n }\n\n public function coachingMessages(): HasMany\n {\n return $this->hasMany(Message::class)\n ->where('is_private', 1);\n }\n\n public function getCoachingMessages(): Eloquent\\Collection\n {\n return $this->getAttribute('coachingMessages');\n }\n\n public function participants(): HasMany\n {\n return $this->hasMany(Participant::class);\n }\n\n public function getSnapshots(): Eloquent\\Collection\n {\n return $this->getAttribute('snapshots');\n }\n\n /** @return HasMany<Track> */\n public function tracks(): HasMany\n {\n return $this->hasMany(Track::class);\n }\n\n public function tracksWithTelephony(): HasMany\n {\n return $this->hasMany(Track::class)->whereNotNull('telephony_provider_id');\n }\n\n public function getTracksWithTelephony(): Eloquent\\Collection\n {\n return $this->getAttribute('tracksWithTelephony');\n }\n\n /** @return Collection|Track[] */\n public function getTracks(): Eloquent\\Collection\n {\n return $this->getAttribute('tracks');\n }\n\n public function masterTrack(): HasOne\n {\n return $this->hasOne(Track::class)->where('is_master', 1)\n ->whereIn('format', [Track::FORMAT_WAV, Track::FORMAT_M3U8])\n ->latest();\n }\n\n public function getMasterTrack(): ?Track\n {\n /** @var Track|null */\n return $this->getAttribute('masterTrack');\n }\n\n public function transcription(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Transcription::class, 'transcription_id');\n }\n\n public function findTranscriptionPromptSummaries(): Collection\n {\n $transcriptionId = $this->getTranscriptionId();\n if (is_null($transcriptionId)) {\n return new Collection();\n }\n\n return Models\\AiPrompt::query()\n ->where('transcription_id', $transcriptionId)\n ->get();\n }\n\n public function getTranscription(): Transcription\n {\n return $this->getAttribute('transcription');\n }\n\n public function hasTranscription(): bool\n {\n return $this->getAttribute('transcription') !== null;\n }\n\n public function setTranscriptionId(int $transcriptionId): Activity\n {\n $this->setAttribute('transcription_id', $transcriptionId);\n\n return $this;\n }\n\n public function unsetTranscriptionId(): self\n {\n $this->setAttribute('transcription_id', null);\n\n return $this;\n }\n\n public function getTranscriptionId(): ?int\n {\n return $this->getAttribute('transcription_id');\n }\n\n /** @deprecated */\n public function hasTranscriptionId(): bool\n {\n return $this->getAttribute('transcription_id') !== null;\n }\n\n public function coachRequests()\n {\n return $this->hasMany(CoachRequest::class);\n }\n\n public function availabilityNotifications()\n {\n return $this->hasMany(AvailabilityNotification::class);\n }\n\n public function processingStates()\n {\n return $this->hasMany(Models\\Activity\\ActivityProcessingState::class);\n }\n\n public function uploadSettings()\n {\n return $this->hasMany(ActivityUploadSetting::class);\n }\n\n public function comments()\n {\n return $this->hasMany(Comment::class);\n }\n\n public function getComments(): Eloquent\\Collection\n {\n return $this->getAttribute('comments');\n }\n\n public function visibleComments()\n {\n $rel = $this->hasMany(Comment::class);\n // Doesn't have auth()->user() in some tests, breaks the build\n if ($user = auth()->user()) {\n return $rel->visibleThreads($user->id);\n }\n\n return $rel;\n }\n\n public function snapshots(): HasMany\n {\n return $this->hasMany(Snapshot::class);\n }\n\n public function calendarEvent()\n {\n return $this->belongsTo(CalendarEvent::class);\n }\n\n public function getCalendarEvent(): ?CalendarEvent\n {\n return $this->getAttribute('calendarEvent');\n }\n\n public function latestCoachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class)->latest();\n }\n\n public function playlists(): BelongsToMany\n {\n return $this->belongsToMany(Playlist::class, 'playlist_activities')\n ->withPivot('id', 'uuid', 'user_id', 'start_time', 'end_time')\n ->using(PlaylistActivity::class)\n ->withTimestamps();\n }\n\n public function coachingFeedbacks(): HasMany\n {\n return $this->hasMany(CoachingFeedback::class);\n }\n\n /**\n * @return Eloquent\\Collection|CoachingFeedback[]\n */\n public function getCoachingFeedback(?int $visibility = null): Eloquent\\Collection\n {\n $feedbacks = $this->coachingFeedbacks();\n if ($visibility !== null) {\n $feedbacks = $feedbacks->where('visibility', $visibility);\n }\n\n return $feedbacks->get();\n }\n\n /** @return Eloquent\\Collection<int, PlaylistActivity> */\n public function favoritedBy(User $user): Eloquent\\Collection\n {\n return $this->favorites()->where('user_id', $user->getId())->get();\n }\n\n /**\n * Checks whether consumer has added this activity to their favorites playlist\n * In addition a default playlist gets created if not already present\n */\n public function wasFavoritedBy(User $user): bool\n {\n $playlist = $user->favoritePlaylist();\n\n return $playlist\n ->activities()\n ->where('activity_id', '=', $this->getId())\n ->exists();\n }\n\n /**\n * @return HasMany<PlaylistActivity>\n */\n public function playlistActivities(): HasMany\n {\n return $this->hasMany(PlaylistActivity::class);\n }\n\n /**\n * @return HasManyThrough<Playlist>\n */\n public function favoritePlaylists(): HasManyThrough\n {\n return $this->hasManyThrough(\n Playlist::class,\n PlaylistActivity::class,\n 'activity_id',\n 'id',\n 'id',\n 'playlist_id'\n )->where('is_default', 1);\n }\n\n /**\n * @return Eloquent\\Collection<int, Playlist>\n */\n public function getFavoritePlaylists(): Eloquent\\Collection\n {\n return $this->getAttribute('favoritePlaylists');\n }\n\n /**\n * Get activities from the default/favorite playlist\n *\n * @return Eloquent\\Builder|static\n */\n public function favorites()\n {\n return $this->playlistActivities()->whereHas('playlist', function ($query) {\n $query->where('is_default', 1);\n });\n }\n\n /**\n * @return Model|SubscriptionSet|null|object\n */\n public function subscribedBy(User $user)\n {\n if ($this->prospect === null) {\n return null;\n }\n\n return SubscriptionSet::select('activity_subscription_sets.*')\n ->where('user_id', $user->id)\n ->join('activity_subscriptions', function ($join) {\n $join\n ->on('subscription_set_id', '=', 'activity_subscription_sets.id');\n\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $join\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $join\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $join\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $join\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n }\n })\n ->first();\n }\n\n /**\n * @return array|Eloquent\\Builder[]|Eloquent\\Collection|SubscriptionSet[]\n */\n public function subscribers()\n {\n if ($this->prospect === null) {\n return [];\n }\n\n return SubscriptionSet::with(['subscriptions', 'user'])\n ->whereHas('subscriptions', function ($query) {\n if ($this->account_id) {\n if ($this->opportunity_id) {\n $query\n ->where('followable_type', 'opportunity')\n ->where('followable_id', $this->opportunity_id);\n } else {\n $query\n ->where('followable_type', 'account')\n ->where('followable_id', $this->account_id);\n }\n } elseif ($this->contact_id) {\n $query\n ->where('followable_type', 'contact')\n ->where('followable_id', $this->contact_id);\n } elseif ($this->lead_id) {\n $query\n ->where('followable_type', 'lead')\n ->where('followable_id', $this->lead_id);\n } else {\n // Nothing to join on?\n // refactor - use Jiminny specific exception\n throw new InvalidArgumentException('Cannot join on a specific customer filter.');\n }\n })\n ->whereHas('user', function ($query) {\n $query\n ->where('team_id', $this->user->team_id)\n ->where('status', User::STATUS_ACTIVE);\n })\n ->get();\n }\n\n /**\n * @return HasMany|Builder|Eloquent\\Collection|Play[]\n */\n public function plays()\n {\n return $this->hasMany(Play::class);\n }\n\n public function getPlays(): Eloquent\\Collection\n {\n return $this->getAttribute('plays');\n }\n\n public function playsBy(User $user)\n {\n /** @var Builder $builder */\n $builder = $this->plays()->where('user_id', $user->id);\n\n return $builder->get();\n }\n\n /**\n * Check if activity was played by a user\n */\n public function wasPlayedBy(User $user): bool\n {\n return $this->plays()->where('user_id', $user->id)->exists();\n }\n\n public function shares()\n {\n return $this->hasMany(Share::class);\n }\n\n /** @return BelongsTo<Participant, self> */\n public function from(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'from_participant_id');\n }\n\n /** @return BelongsTo<Participant, self> */\n public function to(): BelongsTo\n {\n return $this->belongsTo(Participant::class, 'to_participant_id');\n }\n\n /**\n * Get all of the connections through the participants.\n */\n public function connections()\n {\n return $this->hasManyThrough(\n Connection::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getConnections(): Eloquent\\Collection\n {\n return $this->getAttribute('connections');\n }\n\n /**\n * Get all of the shares through the participants.\n */\n public function participantShares()\n {\n return $this->hasManyThrough(\n Participant\\Share::class,\n Participant::class,\n 'activity_id',\n 'participant_id'\n );\n }\n\n public function getParticipantShares(): Eloquent\\Collection\n {\n return $this->getAttribute('participantShares');\n }\n\n public function topicTriggers(): HasMany\n {\n return $this->hasMany(TopicTrigger::class);\n }\n\n public function activityScorecardRuleTriggers(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRuleTrigger::class);\n }\n\n public function activityScorecardRules(): HasMany\n {\n return $this->hasMany(Models\\Scorecard\\ActivityScorecardRule::class);\n }\n\n public function questions(): HasMany\n {\n return $this->hasMany(Question::class);\n }\n\n /**\n * Get all the custom data attached to it.\n */\n public function data(): HasMany\n {\n return $this->hasMany(FieldData::class);\n }\n\n public function getData(): Eloquent\\Collection\n {\n /** @var Eloquent\\Collection */\n return $this->getAttribute('data');\n }\n\n #[Scope]\n protected function heldBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('actual_start_date', '>=', $from)\n ->where('actual_end_date', '<=', $until);\n }\n\n #[Scope]\n protected function scheduledBetween($query, Carbon $start, Carbon $end)\n {\n // Sanity check.\n $from = min($start, $end);\n $until = max($start, $end);\n\n return $query\n ->where('scheduled_start_date', '>=', $from)\n ->where('scheduled_end_date', '<=', $until);\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function forTeam(Builder $query, int $teamId): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas('user', static function (Builder $query) use ($teamId): void {\n $query->where('team_id', $teamId);\n });\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function inOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->whereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query\n ->where('is_closed', false)\n ->where('deleted_at', '=', null),\n );\n }\n\n /**\n * @param Builder<self> $query\n *\n * @return Builder<self>\n */\n #[Scope]\n protected function notInOpenDeals(Builder $query): Builder\n {\n /** @var Builder<self> */\n return $query->where(\n static fn (Builder $query): Builder => $query->whereNull('opportunity_id')\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->where('is_closed', true),\n )\n ->orWhereHas(\n 'opportunity',\n static fn (Builder $query): Builder => $query->withTrashed()->where('deleted_at', '!=', null),\n ),\n );\n }\n\n /**\n * Finds a participant and updates it with data. If participant doesn't exist creates a new participant from data.\n *\n * @param array $data participant data used to identify the participant and update it\n * @param bool $enterRoom true if participant is entering the room. false if we just want to update some participant data\n * @param Carbon|null $enterTime if $enterNow is true then this is the join time when the actual enter has occurred\n */\n public function updateOrCreateParticipant(\n array $data,\n bool $enterRoom = true,\n ?Carbon $enterTime = null,\n bool $nameMatching = false,\n ): Participant {\n $search = [];\n $participant = null;\n\n if (isset($data['user_id'])) {\n // Check if they already exist based on their ID.\n $search['user_id'] = $data['user_id'];\n } elseif (isset($data['provider_id'])) {\n $search['provider_id'] = $data['provider_id'];\n } elseif ($nameMatching && isset($data['name'])) {\n $search['name'] = $data['name'];\n }\n\n if (! empty($data['email'])) {\n $search['email'] = $data['email'];\n\n // If we have their email, this should be unique enough to lookup (e.g. calendar event based participant).\n unset($search['provider_id']);\n }\n\n // Search by phone number only in case nothing else is available to search by.\n if (array_key_exists('phone_number', $data) && empty($search)) {\n $search['phone_number'] = $data['phone_number'];\n }\n\n if (! empty($search)) {\n // Do a lookup now to see if we have a match on the provided data.\n $lookup = array_map(static function ($key, $value): array {\n return [$key, $value];\n }, array_keys($search), $search);\n\n $participant = $this->participants()->withTrashed()->where($lookup)->first();\n }\n\n // Do a partial match on the name and search in the team members.\n if (! $participant instanceof Participant && $nameMatching && ! empty($data['name'])) {\n $participantMatcher = app(MeetingBot\\Service\\ParticipantMatcher::class);\n\n if (! $participantMatcher instanceof MeetingBot\\Service\\ParticipantMatcher) {\n throw new LogicException('Expecting ParticipantMatcher service instance');\n }\n\n $participant = $participantMatcher->match($this, $data['name']);\n\n // If we've found good participant, avoid data overwrite in `$participant->fill($data)` below.\n if ($participant instanceof Models\\Participant && $participant->hasName()) {\n unset($data['name']); // Thoughts: should we unset also $data['user_id'] and $data['email'] ?\n }\n }\n\n if (! $participant instanceof Participant) {\n // If no match, create a new participant.\n if (empty($search)) {\n $participant = $this->participants()->create();\n } else {\n // If no match, create a new participant but avoid creating duplicates\n $participant = $this->participants()->withTrashed()->firstOrNew($search);\n }\n }\n\n // If we have just recycled a deleted participant\n if ($participant->trashed()) {\n $participant->deleted_at = null;\n }\n\n // Deal with the case when calendar syncs the event while it's in progress.\n // We should prevent change of the participant name, because speeches mapping will fail.\n if ($enterRoom === false\n && $this->isInProgress()\n && $participant->hasName()\n && isset($data['name'])\n && $data['name'] !== $participant->getName()\n ) {\n unset($data['name']);\n }\n\n // Upsert with new data.\n $participant->fill($data);\n\n if ($enterRoom) {\n if ($enterTime === null) {\n $enterTime = now();\n }\n\n // Participant enters room for the first time\n if ($participant->enter_time === null) {\n $participant->enter_time = $enterTime;\n }\n\n // If there is an exit time and it's prior to new enter_time\n if ($participant->exit_time && $participant->exit_time->lt($enterTime)) {\n // Participant has re-joined\n $participant->exit_time = null;\n }\n }\n\n $participant->save();\n\n return $participant;\n }\n\n /**\n * Updates participant CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n * @param Participant $participant participant the CRM data is associated with\n */\n public function updateParticipantCrmData(array $records, Participant $participant): void\n {\n // Extract the records.\n [$lead, , , $contact] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForParticipant($lead, $contact);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n if (! $participant->hasName()) {\n $participant->name = $lead->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $lead->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $lead->phone;\n }\n\n $participant->lead_id = $lead->id;\n $participant->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n if (! $participant->hasName()) {\n $participant->name = $contact->name;\n }\n\n if (! $participant->hasEmailAddress()) {\n $participant->email = $contact->email;\n }\n\n if (! $participant->hasPhoneNumber()) {\n $participant->phone_number = $contact->phone;\n }\n\n $participant->contact_id = $contact->id;\n $participant->save();\n }\n }\n\n /**\n * Updates activity CRM data\n *\n * @param array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *} $records\n */\n public function updateActivityCrmData(array $records): void\n {\n // Extract the records.\n [$lead, $account, $opportunity, $contact, $stage] = $records;\n\n $resolver = $this->getUpdateCrmDataResolver();\n $strategy = $resolver->resolveForActivity($lead, $contact, $account);\n\n if ($strategy == UpdateCrmDataByStrategy::Lead) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n if ($this->account_id === null && $this->contact_id === null && $this->lead_id === null) {\n $this->lead_id = $lead->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n $this->save();\n }\n } elseif ($strategy == UpdateCrmDataByStrategy::Contact) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Don't trust previous matched account_id as it might have been changed in the CRM\n if ($account && $account->id !== $this->account_id) {\n $this->account_id = $account->id;\n }\n\n if ($opportunity && $this->opportunity_id !== $opportunity->id) {\n $this->opportunity_id = $opportunity->id;\n // Reset stage_id when opportunity changes to avoid stale stage from previous opportunity match\n $this->stage_id = $stage?->id;\n }\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($opportunity && $this->value !== $opportunity->value) {\n $this->value = $opportunity->value;\n }\n\n // Always set contact_id when available, regardless of account_id status\n if ($this->contact_id === null && $contact) {\n $this->contact_id = $contact->id;\n }\n\n $this->save();\n } elseif ($strategy == UpdateCrmDataByStrategy::Account && $this->account_id === null) {\n // Also update the parent activity if required, checking we don't create a mixed lead/account record.\n $this->lead_id = null;\n if ($this->stage && $this->stage->getType() === Stage::TYPE_LEAD) {\n $this->stage_id = null;\n }\n\n // Update the account and opportunity on the activity record if possible.\n $this->account_id = $account->id;\n\n if ($this->stage_id === null && $stage) {\n $this->stage_id = $stage->id;\n }\n\n if ($this->opportunity_id === null && $opportunity) {\n $this->opportunity_id = $opportunity->id;\n $this->value = $opportunity->value;\n }\n\n $this->save();\n }\n }\n\n public function getActivityProspectData(): array\n {\n return [\n 'lead' => $this->lead_id,\n 'contact' => $this->contact_id,\n 'account' => $this->account_id,\n 'opportunity' => $this->opportunity_id,\n 'stage' => $this->stage_id,\n ];\n }\n\n public function isOrganizer(User $user): bool\n {\n return $this->user_id && $this->user_id === $user->id;\n }\n\n public function isJoinable(): bool\n {\n return \\in_array($this->status, [\n self::STATUS_SCHEDULED,\n self::STATUS_PENDING,\n self::STATUS_RINGING,\n self::STATUS_IN_PROGRESS,\n ], true);\n }\n\n public function isAttemptedForBotJoin(): bool\n {\n return in_array($this->getAttribute('status'), self::MEETING_BOT_JOIN_ATTEMPTED, true);\n }\n\n /**\n * Check if the activity can be saved to CRM (manual or autolog)\n */\n public function isLoggable(): bool\n {\n if ($this->getUser()->getTeam()->hasFeature(FeatureEnum::SIDEKICK_SETTINGS)) {\n $sidekickService = app(SidekickService::class);\n\n if (! $sidekickService->isSidekickEnabledForUser($this->getUser())) {\n return false;\n }\n }\n\n // If we don't know the activity type, don't try to log.\n if ($this->playbook_category_id === null) {\n return false;\n }\n\n if ($this->user->crm_required === false) {\n return false;\n }\n\n // Don't prompt for internal meetings.\n if ($this->is_internal) {\n return false;\n }\n\n // If we don't know who we are trying to log to, don't try to log.\n if ($this->prospect === null) {\n return false;\n }\n\n $validStatus = false;\n switch ($this->type) {\n case self::TYPE_SOFTPHONE:\n case self::TYPE_SOFTPHONE_INBOUND:\n $validStatus = true;\n\n break;\n case self::TYPE_CONFERENCE:\n $validStatus = in_array($this->status, [\n self::STATUS_BUSY,\n self::STATUS_NO_ANSWER,\n self::STATUS_COMPLETED,\n self::STATUS_CANCELLED,\n ], true);\n\n break;\n case self::TYPE_SMS_INBOUND:\n case self::TYPE_SMS_OUTBOUND:\n $validStatus = in_array($this->status, [\n self::STATUS_QUEUED,\n self::STATUS_SENT,\n self::STATUS_UNDELIVERED,\n self::STATUS_DELIVERED,\n self::STATUS_RECEIVED,\n ], true);\n\n break;\n }\n\n // Depending on the activity channel, we should not try to log.\n return $validStatus;\n }\n\n public function isScheduled(): bool\n {\n return $this->status === self::STATUS_SCHEDULED;\n }\n\n public function scheduledDuration(): int\n {\n if ($this->scheduled_start_time && $this->scheduled_end_time) {\n return $this->scheduled_end_time->timestamp - $this->scheduled_start_time->timestamp;\n }\n\n return 0;\n }\n\n public function isPending(): bool\n {\n return $this->status === self::STATUS_PENDING;\n }\n\n public function isCompleted(): bool\n {\n return $this->status === self::STATUS_COMPLETED;\n }\n\n public function isRinging(): bool\n {\n return $this->status === self::STATUS_RINGING;\n }\n\n public function isInProgress(): bool\n {\n return $this->status === self::STATUS_IN_PROGRESS;\n }\n\n public function isBusy(): bool\n {\n return $this->status === self::STATUS_BUSY;\n }\n\n public function isNoAnswer(): bool\n {\n return $this->status === self::STATUS_NO_ANSWER;\n }\n\n public function isFailed(): bool\n {\n return $this->status === self::STATUS_FAILED;\n }\n\n public function isCancelled(): bool\n {\n return $this->status === self::STATUS_CANCELLED;\n }\n\n public function hasEnded(int $gracePeriodMinutes = 15): bool\n {\n if ($this->isCompleted()) {\n return true;\n }\n\n if (($this->isFailed() || $this->isCancelled()) && $this->hasScheduledEndTime()) {\n return $this->getScheduledEndTime()->addMinutes($gracePeriodMinutes)->isPast();\n }\n\n return false;\n }\n\n public function hasStarted(): bool\n {\n return $this->hasActualStartTime();\n }\n\n public function isOngoing(): bool\n {\n return $this->hasActualStartTime() && ! $this->hasActualEndTime();\n }\n\n public function isTypeSmsInbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_INBOUND;\n }\n\n public function isTypeSmsOutbound(): bool\n {\n return $this->getType() === self::TYPE_SMS_OUTBOUND;\n }\n\n public function isTypeSoftPhone(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE;\n }\n\n public function isTypeSoftphoneInbound(): bool\n {\n return $this->getType() === self::TYPE_SOFTPHONE_INBOUND;\n }\n\n public function isTypeConference(): bool\n {\n return $this->getType() === self::TYPE_CONFERENCE;\n }\n\n /**\n * Get a conference elapsed time in seconds.\n *\n * @return int seconds count\n */\n public function secondsTimeElapsed(): int\n {\n if (empty($this->actual_start_time)) {\n return 0;\n }\n\n // Get number of seconds since conference actual start time\n return (int) abs(Carbon::now()->diffInRealSeconds($this->actual_start_time));\n }\n\n /**\n * Get a conference elapsed time formatted as \"1:30:20\" if more than 1 hour or \"30:20\" otherwise.\n */\n public function formattedTimeElapsed(): string\n {\n // Get number of seconds since conference actual start time.\n $elapsedSeconds = $this->secondsTimeElapsed();\n $elapsedTime = Carbon::createFromTimestampUTC($elapsedSeconds);\n\n // Format conference start time.\n return $elapsedTime->format($elapsedSeconds < 3600 ? 'i:s' : 'G:i:s');\n }\n\n public function wasScheduled(): bool\n {\n return $this->calendarEvent !== null || in_array($this->getSource(), [self::SOURCE_OUTLOOK, self::SOURCE_GOOGLE]);\n }\n\n public function isInstant(): bool\n {\n return ! $this->wasScheduled();\n }\n\n /**\n * GETTERS AND SETTERS FOLLOW BELOW\n */\n\n public function getUuid(): string\n {\n return $this->getAttribute('id_string');\n }\n\n public function getId(): int\n {\n return $this->getAttribute('id');\n }\n\n public function getFromParticipantId(): ?int\n {\n return $this->getAttribute('from_participant_id');\n }\n\n public function getFromParticipant(): ?Participant\n {\n return $this->getAttribute('from');\n }\n\n public function getToParticipantId(): ?int\n {\n return $this->getAttribute('to_participant_id');\n }\n\n public function getToParticipant(): ?Participant\n {\n return $this->getAttribute('to');\n }\n\n public function hasScheduledStartTime(): bool\n {\n return $this->getAttribute('scheduled_start_time') !== null;\n }\n\n public function getScheduledStartTime(): ?Carbon\n {\n return $this->getAttribute('scheduled_start_time');\n }\n\n public function setScheduledStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_start_time', $dateTime);\n\n return $this;\n }\n\n public function getScheduledEndTime(): ?DateTimeInterface\n {\n return $this->getAttribute('scheduled_end_time');\n }\n\n public function hasScheduledEndTime(): bool\n {\n return $this->getAttribute('scheduled_end_time') !== null;\n }\n\n public function setScheduledEndTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('scheduled_end_time', $dateTime);\n\n return $this;\n }\n\n public function getActualStartTime(): ?Carbon\n {\n return $this->getAttribute('actual_start_time');\n }\n\n public function hasActualStartTime(): bool\n {\n return $this->getAttribute('actual_start_time') !== null;\n }\n\n public function getActualEndTime(): ?Carbon\n {\n return $this->getAttribute('actual_end_time');\n }\n\n public function hasActualEndTime(): bool\n {\n return $this->getAttribute('actual_end_time') !== null;\n }\n\n public function getType(): ?string\n {\n return $this->getAttribute('type');\n }\n\n public function getStatus(): string\n {\n return $this->getAttribute('status');\n }\n\n public function setStatus(string $status): self\n {\n $this->setAttribute('status', $status);\n\n return $this;\n }\n\n public function setActualStartTime(DateTimeInterface $dateTime): self\n {\n $this->setAttribute('actual_start_time', $dateTime);\n\n return $this;\n }\n\n public function setActualEndTime(DateTimeInterface $dateTime, bool $shouldUpdateDuration = true): self\n {\n $this->setAttribute('actual_end_time', $dateTime);\n\n if (! $shouldUpdateDuration) {\n return $this;\n }\n\n return $this->updateDuration();\n }\n\n public function updateDuration(): self\n {\n if (! $this->hasActualStartTime() || ! $this->hasActualEndTime()) {\n return $this;\n }\n\n return $this->setDuration(\n (int) abs($this->getActualStartTime()->diffInRealSeconds($this->getActualEndTime()))\n );\n }\n\n public function setDuration(int $duration): self\n {\n $this->setAttribute('duration', $duration);\n\n return $this;\n }\n\n public function getRecordingState(): string\n {\n return $this->getAttribute('recording_state');\n }\n\n public function isRecordingState(string $recordingState): bool\n {\n return $this->getRecordingState() === $recordingState;\n }\n\n public function setRecordingState(string $recordingState): self\n {\n $this->setAttribute('recording_state', $recordingState);\n\n return $this;\n }\n\n public function hasActivityType(): bool\n {\n return $this->getAttribute('category') !== null;\n }\n\n public function getActivityType(): ?PlaybookCategory\n {\n return $this->getAttribute('category');\n }\n\n public function setActivityType(int $playbookCategoryId): self\n {\n $this->setAttribute('playbook_category_id', $playbookCategoryId);\n\n return $this;\n }\n\n public function hasStage(): bool\n {\n return $this->getAttribute('stage') !== null;\n }\n\n public function getStage(): ?Stage\n {\n return $this->getAttribute('stage');\n }\n\n public function getStageId(): ?int\n {\n return $this->getAttribute('stage_id');\n }\n\n public function setStageId(?int $stageId): void\n {\n $this->setAttribute('stage_id', $stageId);\n }\n\n public function hasOpportunity(): bool\n {\n return $this->getAttribute('opportunity') !== null;\n }\n\n public function getOpportunity(): ?Opportunity\n {\n return $this->getAttribute('opportunity');\n }\n\n public function getOpportunityId(): ?int\n {\n return $this->getAttribute('opportunity_id');\n }\n\n public function setOpportunityId(?int $opportunityId): void\n {\n $this->setAttribute('opportunity_id', $opportunityId);\n }\n\n public function hasContact(): bool\n {\n return $this->getAttribute('contact') !== null;\n }\n\n public function getContact(): ?Contact\n {\n return $this->getAttribute('contact');\n }\n\n public function getContactId(): ?int\n {\n return $this->getAttribute('contact_id');\n }\n\n public function setContactId(?int $contactId): void\n {\n $this->setAttribute('contact_id', $contactId);\n }\n\n public function hasLead(): bool\n {\n return $this->getAttribute('lead') !== null;\n }\n\n public function getLead(): ?Lead\n {\n return $this->getAttribute('lead');\n }\n\n public function getLeadId(): ?int\n {\n return $this->getAttribute('lead_id');\n }\n\n public function setLeadId(?int $leadId): void\n {\n $this->setAttribute('lead_id', $leadId);\n }\n\n public function hasAccount(): bool\n {\n return $this->getAttribute('account') !== null;\n }\n\n public function getAccount(): ?Account\n {\n return $this->getAttribute('account');\n }\n\n public function getAccountId(): ?int\n {\n return $this->getAttribute('account_id');\n }\n\n public function setAccountId(?int $accountId): void\n {\n $this->setAttribute('account_id', $accountId);\n }\n\n /**\n * This method exists to avoid confusion using ->participants() or ->participants. Use the getter instead.\n *\n * @return Collection<int, Participant>|Participant[]\n */\n public function getParticipants(): Collection\n {\n return $this->participants;\n }\n\n /**\n * @deprecated use ParticipantRepository::findParticipantRoomOwner() instead\n */\n public function findParticipantRoomOwner(): ?Participant\n {\n $roomOwnerId = $this->getUserId();\n\n return $this->getParticipants()\n ->filter(static fn (Participant $participant): bool => $participant->isSameUserId($roomOwnerId))\n ->first();\n }\n\n public function hasCrmProviderId(): bool\n {\n return $this->getAttribute('crm_provider_id') !== null;\n }\n\n public function getCrmProviderId(): ?string\n {\n return $this->getAttribute('crm_provider_id');\n }\n\n public function setCrmProviderId(?string $crmProviderId): void\n {\n $this->setAttribute('crm_provider_id', $crmProviderId);\n }\n\n public function getUserId(): ?int\n {\n return $this->getAttribute('user_id');\n }\n\n public function hasUser(): bool\n {\n return $this->user()->exists();\n }\n\n public function getUser(): User\n {\n return $this->getAttribute('user');\n }\n\n public function getCreatedAt(): Carbon\n {\n return $this->getAttribute('created_at');\n }\n\n public function isInFiniteState(): bool\n {\n return $this->isFiniteState($this->getStatus());\n }\n\n public function isFiniteState(string $status): bool\n {\n $finiteStates = self::FINITE_STATES[$this->getType()] ?? [];\n\n return in_array($status, $finiteStates, true);\n }\n\n public function getParticipant(Authenticatable $user): Participant\n {\n return $this->findParticipant($user);\n }\n\n public function findParticipant(Authenticatable $user): ?Participant\n {\n if ($user instanceof User) {\n /** @var User $user */\n return $this->participants()->where('user_id', '=', $user->getId())->first();\n }\n\n throw new LogicException(sprintf('Unsupported Authenticatable implementation %s', get_class($user)));\n }\n\n public function hasLanguageCode(): bool\n {\n return $this->getAttribute('language') !== null;\n }\n\n public function getLanguageCode(): ?string\n {\n /** @var string|null */\n return $this->getAttribute('language');\n }\n\n public function getLanguageCodeHyphenated(): string\n {\n return str_replace('_', '-', $this->getLanguageCode() ?? '');\n }\n\n public function getLanguageCodeLocale(): string\n {\n [ $language ] = explode('_', $this->getLanguageCode() ?? '');\n\n return $language;\n }\n\n public function setLanguageCode(string $value): self\n {\n return $this->setAttribute('language', $value);\n }\n\n public function hasSource(): bool\n {\n return $this->getAttribute('source') !== null;\n }\n\n public function setSource(?string $source): self\n {\n return $this->setAttribute('source', $source);\n }\n\n public function isSource(string $source): bool\n {\n return $this->getAttribute('source') === $source;\n }\n\n public function getSource(): ?string\n {\n return $this->getAttribute('source');\n }\n\n public function isSourceGong(): bool\n {\n return $this->isSource(self::SOURCE_GONG);\n }\n\n public function getExternalId(): ?string\n {\n return $this->getAttribute('external_id');\n }\n\n public function setExternalId(?string $externalId): self\n {\n return $this->setAttribute('external_id', $externalId);\n }\n\n public function hasExternalId(): bool\n {\n return $this->getAttribute('external_id') !== null;\n }\n\n public function getProvider(): string\n {\n return $this->getAttribute('provider');\n }\n\n public function hasTelephonyProviderId(): bool\n {\n return $this->getAttribute('telephony_provider_id') !== null;\n }\n\n public function getTelephonyProviderId(): ?string\n {\n return $this->getAttribute('telephony_provider_id');\n }\n\n public function setTelephonyProviderId(?string $telephonyProviderId): self\n {\n return $this->setAttribute('telephony_provider_id', $telephonyProviderId);\n }\n\n public function getLocation(): ?string\n {\n return $this->getAttribute('location');\n }\n\n public function setLocation(?string $location): self\n {\n return $this->setAttribute('location', $location);\n }\n\n public function isDeleted(): bool\n {\n return $this->getAttribute('deleted_at') !== null;\n }\n\n /**\n * Check if activity recording is on and activity status is not one of the failed statuses.\n */\n public function canReviewActivity(): bool\n {\n $failedStatuses = self::$enumFailedStatuses;\n\n return (! in_array($this->recording_state, [self::RECORDING_OFF, self::RECORDING_STOPPED], true) &&\n ! in_array($this->status, $failedStatuses, true));\n }\n\n public function hasReasonCodeBotKicked(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_MEETING_BOT_KICKED);\n }\n\n public function hasReasonCodeNotCompliant(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_CONSENT_DENIED);\n }\n\n public function hasTopicTriggers(): bool\n {\n return $this->topicTriggers()->count() !== 0;\n }\n\n public function getTopicTriggers(): Collection\n {\n return $this->topicTriggers;\n }\n\n public function getTopicTriggersSorted(): Collection\n {\n $this->loadMissing([\n 'topicTriggers.participant',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic',\n 'topicTriggers.playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme',\n ]);\n\n return $this->topicTriggers\n ->sortBy([\n 'playbackThemeTopicTrigger.playbackThemeTopic.playbackTheme.sort',\n 'playbackThemeTopicTrigger.playbackThemeTopic.sort',\n 'playbackThemeTopicTrigger.sort',\n ]);\n }\n\n public function hasQuestions(): bool\n {\n return $this->questions()->exists();\n }\n\n public function getQuestions(): Collection\n {\n return $this->questions;\n }\n\n public function hasValue(): bool\n {\n return $this->getAttribute('value') !== null;\n }\n\n public function getValue(): ?float\n {\n return $this->getAttribute('value');\n }\n\n public function setValue(?float $value): void\n {\n $this->setAttribute('value', $value);\n }\n\n public function transitionTo(string $newState, callable $callback, ?int $timeout = null): self\n {\n $newState = $this->getWorkflowStateFor(\n $this->getType(),\n $newState\n );\n\n return $this->traitTransitionTo($newState, $callback, $timeout);\n }\n\n public function getWorkflowState(): string\n {\n return $this->getWorkflowStateFor(\n $this->getType(),\n $this->getStatus()\n );\n }\n\n public function getActivityProviderDisplayName(): string\n {\n return \\Cache::remember('activity_provider_display_name-' . $this->getProvider(), 60 * 60 * 24, function () {\n $activityProviderRegistry = app()->make(ActivityProviderRegistry::class);\n\n try {\n return $activityProviderRegistry->get($this->getProvider())->getDisplayName();\n } catch (Exception $exception) {\n return ucfirst($this->getProvider());\n }\n });\n }\n\n private function getWorkflowStateFor(string $activityChannel, string $activityStatus): string\n {\n return sprintf(\n '%s::%s',\n $activityChannel,\n $activityStatus\n );\n }\n\n public function getWorkflow(): array\n {\n $map = [\n self::TYPE_SOFTPHONE => [\n self::STATUS_SCHEDULED => [\n self::STATUS_PENDING,\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_PENDING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_RINGING => [\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_IN_PROGRESS,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n self::TYPE_SOFTPHONE_INBOUND => [\n self::STATUS_RINGING => [\n self::STATUS_IN_PROGRESS,\n self::STATUS_NO_ANSWER,\n self::STATUS_CANCELLED,\n self::STATUS_FAILED,\n self::STATUS_BUSY,\n ],\n self::STATUS_IN_PROGRESS => [\n self::STATUS_COMPLETED,\n ],\n ],\n ];\n\n return collect($map)\n ->mapWithKeys(function (array $currentStates, string $activityChannel): array {\n return [\n $activityChannel => collect($currentStates)\n ->mapWithKeys(function (array $possibleStates, $currentState) use ($activityChannel): array {\n $transitionName = $this->getWorkflowStateFor($activityChannel, $currentState);\n\n return [\n $transitionName => array_map(function (string $newState) use ($activityChannel) {\n return $this->getWorkflowStateFor($activityChannel, $newState);\n }, $possibleStates),\n ];\n }),\n ];\n })\n ->reduce(static function (array $carry, Collection $item): array {\n return array_merge($carry, $item->all());\n }, []);\n }\n\n public function hasPosterPath(): bool\n {\n return $this->getAttribute('poster_path') !== null;\n }\n\n public function getPosterPath(): ?string\n {\n return $this->getAttribute('poster_path');\n }\n\n /**\n * Take into account all recording settings and determine if we need to record this activity or not.\n */\n public function shouldRecord(): bool\n {\n return $this->determineRecordingReasonCode() === null;\n }\n\n public function determineRecordingReasonCode(): ?int\n {\n // Conference specific decisions.\n if ($this->isTypeConference()) {\n // If they have manually overridden the recording setting to not record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === false) {\n return self::FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE;\n }\n\n // If they have manually overridden the recording setting to record.\n if ($this->hasRecordingPreference() && $this->getRecordingPreference() === true) {\n return null;\n }\n\n // If their team has disabled recording meetings, don't record.\n if ($this->user->team->isConferenceRecordPreferenceDisabled()) {\n return self::FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED;\n }\n\n // If the host has disabled recording meetings, don't record.\n if ($this->user->checkConferenceRecordPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED;\n }\n\n // If it was marked internal...\n if ($this->is_internal) {\n // and their team has disabled recording internal meetings, don't record.\n if (\n $this->user->team->isConferenceRecordPreferenceEnabled()\n && ! $this->user->team->isConferenceRecordInternalPreferenceEnabled()\n ) {\n return self::FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED;\n }\n\n // and the host has disabled recording internal meetings, don't record.\n if ($this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED;\n }\n }\n\n // If it was not scheduled and they disabled internal meetings, we cannot determine if it was internal.\n if ($this->wasScheduled() === false && $this->user->checkConferenceRecordInternalPreference() === false) {\n return self::FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED;\n }\n }\n\n return null;\n }\n\n public function getRecordingReasonCode(): int\n {\n return $this->getAttribute('recording_reason_code');\n }\n\n public function setRecordingReasonCode(int $recordingReasonCode): self\n {\n $this->setAttribute('recording_reason_code', $recordingReasonCode);\n\n return $this;\n }\n\n // Not used today.\n public function getRecordingReasonString(): ?string\n {\n if ($this->hasRecordingReasonCompliancePrompted()) {\n return Team::COMPLIANCE_MODE_RECORDING_PROMPT;\n }\n\n if ($this->hasRecordingReasonComplianceRestricted()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT;\n }\n\n if ($this->hasRecordingReasonComplianceRestrictedToOneSideRecording()) {\n return Team::COMPLIANCE_MODE_RECORDING_RESTRICT_ONE_SIDE;\n }\n\n return null;\n }\n\n public function hasRecordingReasonComplianceRestricted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT);\n }\n\n public function hasRecordingReasonCompliancePrompted(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_PROMPT);\n }\n\n public function hasRecordingReasonComplianceRestrictedToOneSideRecording(): bool\n {\n return $this->getFlag('recording_reason_code', self::FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE);\n }\n\n public function getAudioTrack(): ?Track\n {\n /** @var Track|null */\n return $this->tracks()\n ->where('type', '=', Track::TYPE_AUDIO)\n ->first();\n }\n\n public function activeParticipants(): HasMany\n {\n return $this->hasMany(Participant::class)->active();\n }\n\n public function getActiveParticipants(): Eloquent\\Collection\n {\n return $this->getAttribute('activeParticipants');\n }\n\n public function crm(): Eloquent\\Relations\\BelongsTo\n {\n return $this->belongsTo(Configuration::class, 'crm_configuration_id');\n }\n\n public function activitySummaryLogs(): HasMany\n {\n return $this->hasMany(ActivitySummaryLog::class);\n }\n\n public function getCrm(): ?Configuration\n {\n return $this->getAttribute('crm');\n }\n\n public function hasCrmConfiguration(): bool\n {\n return $this->getAttribute('crm') !== null;\n }\n\n public function isProcessed(): ?bool\n {\n return $this->getAttribute('is_processed');\n }\n\n public function hasRecordingPrompt(): bool\n {\n return $this->getAttribute('has_recording_prompt') === true;\n }\n\n public function isOnAir(): bool\n {\n return $this->getAttribute('on_air') === self::ON_AIR_READY || $this->getAttribute('on_air') === self::ON_AIR_STREAMING;\n }\n\n public function setOnAir(int $onAir): self\n {\n $this->setAttribute('on_air', $onAir);\n\n return $this;\n }\n\n public function getOnAir(): ?int\n {\n return $this->getAttribute('on_air');\n }\n\n public function setTitleFromCallData(Call $call): void\n {\n $direction = $call->isOutbound() ? 'to' : 'from';\n\n $party = $this->prospect_name\n ?? $call->getContactName()\n ?? $call->getOtherPartyPhoneNumber()\n ;\n\n $this->update(['title' => sprintf('Call %s %s', $direction, $party)]);\n }\n\n /**\n * @param array{}|array{channels:string|null, format:string|null, type:string|null, status:string|null} $audioParams\n */\n public function createAudioTrack(\n string $telephonyProviderId,\n string $recordingUrl,\n array $audioParams = []\n ): Track {\n return $this->tracks()->updateOrCreate([\n 'telephony_provider_id' => $telephonyProviderId,\n ], [\n 'type' => $audioParams['type'] ?? Track::TYPE_AUDIO,\n 'status' => $audioParams['status'] ?? Track::STATUS_PENDING,\n 'format' => $audioParams['format'] ?? Track::FORMAT_WAV,\n 'provider_content_url' => $recordingUrl,\n 'start_time' => $this->actual_start_time,\n 'end_time' => $this->actual_end_time,\n ]);\n }\n\n public function createTrack(string $telephonyProviderId, array $params): Track\n {\n return $this->tracks()->updateOrCreate(\n [\n 'telephony_provider_id' => $telephonyProviderId,\n ],\n $params\n );\n }\n\n public function createOrganiserParticipant(Call $call): Participant\n {\n $user = $this->getUser();\n\n return $this->updateOrCreateParticipant([\n 'is_ghost' => 0,\n 'name' => $user->name,\n 'email' => $user->email,\n 'phone_number' => phone_e164(null, $call->getUserPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'user_id' => $user->id,\n ], false);\n }\n\n public function createProspectParticipant(Call $call): Participant\n {\n // not null 'name' is mandatory here to create a separate participant with 'nameMatching'\n // in case of the same phone_number with the Organiser\n $useNameMatching = $call->getUserPhoneNumber() === $call->getOtherPartyPhoneNumber();\n $defaultName = $useNameMatching ? '' : null;\n\n return $this->updateOrCreateParticipant(data: [\n 'is_ghost' => 0,\n 'name' => $this->prospect->name ?? $defaultName,\n 'email' => $this->prospect->email ?? null,\n 'phone_number' => phone_e164(null, $call->getOtherPartyPhoneNumber()),\n 'enter_time' => $this->actual_start_time,\n 'exit_time' => $this->actual_end_time,\n 'contact_id' => $this->contact_id ?? null,\n 'lead_id' => $this->lead_id ?? null,\n ], enterRoom: false, nameMatching: $useNameMatching);\n }\n\n public function updateParticipants(Participant $organiserParticipant, Participant $prospectParticipant): void\n {\n $this->update([\n 'from_participant_id' => $this->isTypeSoftPhone() ? $organiserParticipant->id : $prospectParticipant->id,\n 'to_participant_id' => $this->isTypeSoftPhone() ? $prospectParticipant->id : $organiserParticipant->id,\n ]);\n }\n\n public function hasProspect(): bool\n {\n return $this->getProspectAttribute() !== null;\n }\n\n public function isPrivate(): bool\n {\n return $this->getAttribute('is_private');\n }\n\n /** Create a new factory instance for the model. */\n protected static function newFactory(): Factory\n {\n return ActivityFactory::new();\n }\n\n public function getUpdatedAt(): Carbon\n {\n return $this->getAttribute('updated_at');\n }\n\n public function getActivitySummaryLogs(): Eloquent\\Collection\n {\n return $this->getAttribute('activitySummaryLogs');\n }\n\n public function hasProspectActivitySummaryLog(): bool\n {\n return $this->getActivitySummaryLogs()->contains(\n 'relation_type',\n ActivitySummaryLog::RELATION_OBJECT_TYPE_PROSPECT\n );\n }\n\n public function getTeam(): Team\n {\n return $this->getUser()->getTeam();\n }\n\n private function getUpdateCrmDataResolver(): UpdateCrmDataResolverInterface\n {\n $factory = app(UpdateCrmDataResolverFactory::class);\n\n return $factory->create($this);\n }\n\n public function getMeetingTrackProviderId(string $type): string\n {\n $label = match ($type) {\n Track::TYPE_VIDEO => 'v',\n Track::TYPE_AUDIO => 'a',\n default => throw new InvalidArgumentJiminnyException('Invalid track type'),\n };\n\n $startTimestamp = $this->getScheduledStartTime()?->getTimestamp();\n $teamId = $this->getTeam()->getId();\n\n return $this->getTelephonyProviderId() . ':' . $label . ':' . $startTimestamp . ':' . $teamId;\n }\n\n /**\n * Get all consent records associated with this activity\n *\n * @return \\Illuminate\\Database\\Eloquent\\Relations\\HasMany\n */\n public function participantConsents(): HasMany\n {\n return $this->hasMany(Participant\\Consent::class);\n }\n\n public function isDiallerCall(): bool\n {\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return false;\n }\n\n if (! in_array($this->getType(), [self::TYPE_SOFTPHONE, self::TYPE_SOFTPHONE_INBOUND])) {\n return false;\n }\n\n return $this->getProvider() !== self::PROVIDER_TWILIO;\n }\n\n public function getActivityDateWithFallback(): Carbon\n {\n if ($this->getActualStartTime() !== null) {\n return $this->getActualStartTime();\n }\n\n if ($this->getScheduledStartTime() !== null) {\n return $this->getScheduledStartTime();\n }\n\n return $this->getCreatedAt();\n }\n\n public function getCrmType(): ?string\n {\n // Treat uploader activities as conferences\n if ($this->getProvider() === Activity::PROVIDER_UPLOADER) {\n return Activity::TYPE_CONFERENCE;\n }\n\n return $this->getType();\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
6769527949084127828
|
6963561001770493756
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
#12077 on JY-20903-update Project: faVsco.js, menu
#12077 on JY-20903-update_activity-stage-on…hange, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
1
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Calendar\Command;
use Exception;
use Illuminate\Events\Dispatcher;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Cache;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Events\Activities\AiAutomation\ActivityProspectAdded;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Stage;
use Jiminny\Models\Participant;
use Jiminny\Models\User;
use Jiminny\Services\Calendar\Adapter\EventAttendee;
use Jiminny\Services\Calendar\Adapter\PersistedEventAttendee;
use Jiminny\Services\Crm\CachedCrmServiceDecorator;
use Jiminny\Services\Crm\EmailHelper;
use Jiminny\Services\Crm\MatchDomainByEmailInterface;
use Jiminny\Services\Crm\ProspectSearchStrategyFactory;
use Jiminny\Services\Crm\RecordSelector;
use Jiminny\Utils\StringUtil;
class ImportParticipants
{
private User $user;
private Activity $activity;
private MatchDomainByEmailInterface|ServiceInterface|null $crmService = null;
/**
* @var array<EventAttendee>
*/
private array $attendees;
private EmailHelper $emailHelper;
private TeamRepository $teamRepository;
private Dispatcher $eventDispatcher;
private array $emailRecords = [];
private array $domainRecords = [];
private array $participantsToKeep = [];
private bool $participantsChanged = false;
public function __construct(
User $user,
Activity $activity,
array $attendees,
EmailHelper $emailHelper,
TeamRepository $teamRepository,
Dispatcher $eventDispatcher,
) {
$this->user = $user;
$this->activity = $activity;
$this->attendees = $attendees;
$this->emailHelper = $emailHelper;
$this->teamRepository = $teamRepository;
$this->eventDispatcher = $eventDispatcher;
}
public function setCrmService(?ServiceInterface $crmService = null): void
{
$this->crmService = $crmService;
}
public function execute(bool $isNewActivity = false): void
{
// save in db and track if participants changed
$this->saveParticipants();
$participantsWereRemoved = false;
if ($this->activity->isScheduled()) {
// Delete the participants who are no longer invited or since declined.
$participantsWereRemoved = $this->purgeRemovedAndDeclinedParticipants();
}
// Only do CRM lookup if this is a new activity or participants changed
$shouldProcessCrm = $isNewActivity || $this->participantsChanged || $participantsWereRemoved;
if ($shouldProcessCrm) {
Log::info('[Calendar] Processing CRM lookup due to participant changes', [
'activity_id' => $this->activity->getUuid(),
'is_new_activity' => $isNewActivity,
'participants_changed' => $this->participantsChanged,
'participants_removed' => $participantsWereRemoved,
]);
// participants crm lookup
$this->processParticipants();
// update activity with crm results
$this->updateActivityCrmData($this->getRecords());
} else {
Log::info('[Calendar] Skipping CRM lookup - no participant changes', [
'activity_id' => $this->activity->getUuid(),
]);
}
}
public function updateActivityCrmData(array $records): void
{
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($this->user->getTeam());
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
// Unset any associated crm Data
Log::info('[Calendar] Ignoring crm data because of prospect strategy', [
'activity_id' => $this->activity->getUuid(),
'strategy' => get_class($prospectSearchStrategy),
]);
// empty the records
$records = [];
}
if (! empty($records)) {
$originalActivityData = $this->activity->getActivityProspectData();
$this->activity->updateActivityCrmData($records);
$this->activity->refresh();
$currentActivityData = $this->activity->getActivityProspectData();
Log::info('[Calendar] Updated activity crm data', [
'activity_id' => $this->activity->getUuid(),
'original_data' => $originalActivityData,
'current_data' => $currentActivityData,
]);
$this->eventDispatcher->dispatch(
new ActivityProspectAdded($this->activity, 'import-participants')
);
}
}
private function getRecords(): array
{
return RecordSelector::pickBestFromLists($this->emailRecords, $this->domainRecords);
}
private function shouldBeProcessed(EventAttendee $attendee): bool
{
// Ensure they are not a meeting resource e.g. a room and have a valid email.
return $attendee->isPerson()
&& filter_var($attendee->email(), FILTER_VALIDATE_EMAIL);
}
private function saveParticipants(): void
{
foreach ($this->attendees as $attendee) {
if (! $this->shouldBeProcessed($attendee)) {
continue;
}
$this->saveParticipant($attendee);
}
}
private function processParticipants(): void
{
foreach ($this->attendees as $attendee) {
if (! $this->shouldBeProcessed($attendee)) {
continue;
}
$this->processAttendee($attendee);
}
}
/**
* PersistedEventAttendee $persistedAttendee here is already saved in the DB participant
*
* @return void
*/
public function refreshCrmData(): void
{
/**
* @var PersistedEventAttendee $persistedAttendee
*/
foreach ($this->attendees as $persistedAttendee) {
$user = $persistedAttendee->getUser();
if ($user === null) {
$user = $this->refreshParticipantUserId($persistedAttendee);
}
$this->participantCrmLookup($persistedAttendee, $user);
}
$this->updateActivityCrmData($this->getRecords());
}
/**
* Save attendee in the DB as a participant
*
* @param EventAttendee $attendee
*
* @return void
*/
private function saveParticipant(EventAttendee $attendee): void
{
$email = $attendee->email();
$lockKey = $this->activity->getUuid() . '_' . $email;
$lock = Cache::lock($lockKey, 1);
if (! $lock->get()) {
Log::warning('[Calendar] Participant already being processed', [
'activity_id' => $this->activity->getUuid(),
'email' => $email,
]);
return;
}
Log::info('[Calendar] Save attendee as a participant', [
'activity_id' => $this->activity->getUuid(),
'email' => $email,
]);
$user = $this->teamRepository->findActiveTeamMemberByEmail($this->user->getTeam(), $email);
// User can have multiple calendars in Google, calendar provider id matches the email
if ($user === null) {
$user = $this->teamRepository->findActiveTeamMemberByCalendarProviderId($this->user->getTeam(), $email);
}
$participantData = [
'user_id' => $user?->getId(),
'email' => $email,
'role' => $attendee->isOrganizer() ? Participant::ROLE_ORGANIZER : null,
];
$this->setParticipantName($participantData, $this->getAttendeeName($attendee, $user));
$participant = $this->activity->updateOrCreateParticipant($participantData, false);
$this->trackParticipantChanges($participant, $email);
if (! $attendee->isDeclined()) {
Log::info('[Calendar] Adding participant to keep list', [
'activity_id' => $this->activity->getUuid(),
'email' => $email,
'participant_id' => $participant->getId(),
]);
$this->participantsToKeep[] = $participant->getId();
}
$lock->release();
}
/**
* Perform a CRM lookup for a participant (attendee)
*
* @param EventAttendee $attendee
*
* @return void
*/
private function processAttendee(EventAttendee $attendee): void
{
$email = $attendee->email();
Log::info('[Calendar] Process attendee', [
'activity_id' => $this->activity->getUuid(),
'email' => $email,
]);
$user = $this->teamRepository->findActiveTeamMemberByEmail($this->user->getTeam(), $email);
$this->participantCrmLookup($attendee, $user);
}
/**
* Perform a CRM lookup and update participant data
*
* @param EventAttendee $attendee
* @param User|null $user
*
* @return void
*/
public function participantCrmLookup(EventAttendee $attendee, ?User $user): void
{
$email = $attendee->email();
$crmRecords = null;
// Don't attempt CRM lookup of attendee if the meeting is internal, or they are the organizer.
// We shouldn't attempt to match any attendees that belong to the user's organisation as well
if ($this->shouldPerformCrmLookup($attendee, $user)) {
Log::info('[Calendar] Prospect attendee found. Attempting to match to a CRM record', [
'activity_id' => $this->activity->getUuid(),
'email' => $email,
]);
$crmRecords = $this->findProspectDataByEmail($email);
}
if (! empty($crmRecords)) {
Log::info('[Calendar] Prospect attendee matched to a CRM record', [
'activity_id' => $this->activity->getUuid(),
'email' => $email,
]);
$participantData = [
'user_id' => $user?->getId(),
'email' => $email,
];
if (empty($this->getAttendeeName($attendee, $user))) {
Log::info('[Calendar] Update participant name from CRM data', [
'activity_id' => $this->activity->getUuid(),
'email' => $email,
]);
// override name from CRM
$this->setParticipantName($participantData, $this->extractNameFromCrmData($crmRecords));
}
// update and get participant
$participant = $this->activity->updateOrCreateParticipant($participantData, false);
Log::info('[Calendar] Updating participant CRM data', [
'activity_id' => $this->activity->getUuid(),
'email' => $email,
'participant_id' => $participant->getId(),
]);
// update participant data from CRM
$this->activity->updateParticipantCrmData($crmRecords, $participant);
}
}
/**
* Refresh Participant userId
* Use case: the participant has not been registered as a User in the time of meeting event creation
* Check for a new registered User with that email and update Participant userId if so
*
* @param PersistedEventAttendee $attendee
*
* @return User|null
*/
private function refreshParticipantUserId(PersistedEventAttendee $attendee): ?User
{
$user = $this->teamRepository->findActiveTeamMemberByEmail($this->user->getTeam(), $attendee->email());
if ($user !== null) {
$participant = $attendee->asParticipant();
$participant->user_id = $user->getId();
$participant->save();
}
return $user;
}
private function getAttendeeName(EventAttendee $attendee, ?User $user): ?string
{
// Attendee display name is an optional field
$name = StringUtil::trimToSize((string) $attendee->displayName(), 100);
if (empty($name)) {
$name = $user?->getName();
}
return $name;
}
private function setParticipantName(array &$participantData, ?string $name): void
{
// If we have a name, set it on the participant
if ($name) {
Log::info('[Calendar] Setting participant name', [
'activity_id' => $this->activity->getUuid(),
'email' => $participantData['email'] ?? null,
'name' => $name,
]);
$participantData['name'] = $name;
}
}
private function shouldPerformCrmLookup(EventAttendee $attendee, ?User $user): bool
{
return ! $attendee->isOrganizer()
&& ! $this->activity->isInternal()
&& ! $this->isOrganisationMember($attendee, $user);
}
private function isOrganisationMember(EventAttendee $attendee, ?User $teamMember): bool
{
return $teamMember instanceof User
|| $this->emailHelper->isCompanyEmail($this->user->getTeam(), $attendee->email());
}
/**
* @param string|null $email
*
* @return null|array{?Lead, ?Account, ?Opportunity, ?Contact, ?Stage, ?string}
*/
private function findProspectDataByEmail(?string $email): ?array
{
try {
// Determine if this participant matches a known lead/contact in the CRM.
$user = $this->activity->getUser();
$userId = $user->getId();
$decorator = resolve(CachedCrmServiceDecorator::class);
$decorator->setCrmService($this->crmService);
$decorator->setConfiguration($user->getTeam()->getCrmConfiguration());
$records = $decorator->matchExactlyByEmail($email, $userId);
if (! empty($records)) {
$this->emailRecords[] = $records;
} else {
$records = $decorator->matchByDomain($email, $userId);
if (! empty($records)) {
$this->domainRecords[] = $records;
}
}
} catch (Exception $exception) {
// If the CRM account is invalid or without a token, just give up matching the attendee.
Log::warning('[Calendar] Failed to match attendee to a CRM record', [
'activityId' => $this->activity->getUuid(),
'email' => $email,
'reason' => $exception->getMessage(),
]);
$records = null;
}
return $records;
}
private function purgeRemovedAndDeclinedParticipants(): bool
{
$participants = $this->activity->participants()->whereNotIn('id', $this->participantsToKeep);
$emails = $participants->pluck('email')->toArray();
if (empty($emails)) {
return false;
}
Log::info('[Calendar] Purging removed and declined participants', [
'activity_id' => $this->activity->getUuid(),
'emails' => $emails,
]);
$participants->delete();
return true;
}
/**
* @param array{?Lead, ?Account, ?Opportunity, ?Contact, ?Stage, ?string} $records
*
* @return string|null
*/
private function extractNameFromCrmData(array $records): ?string
{
[$lead, , , $contact] = $records;
return $lead?->getName() ?? $contact?->getName();
}
/**
* Track if a participant was added or updated to determine if CRM lookup is needed.
* Sets $this->participantsChanged to true if the participant is new or has relevant attribute changes.
* Timestamp fields (created_at, updated_at, deleted_at) are excluded from change detection.
*/
private function trackParticipantChanges(Participant $participant, string $email): void
{
$relevantChanges = array_diff_key(
$participant->getChanges() ?? [],
array_flip(['created_at', 'updated_at', 'deleted_at'])
);
if ($participant->wasRecentlyCreated) {
$this->participantsChanged = true;
Log::info('[Calendar] New participant added', [
'activity_id' => $this->activity->getUuid(),
'email' => $email,
'participant_id' => $participant->getId(),
]);
return;
}
if (! empty($relevantChanges)) {
$this->participantsChanged = true;
Log::info('[Calendar] Participant updated', [
'activity_id' => $this->activity->getUuid(),
'email' => $email,
'participant_id' => $participant->getId(),
'changes' => $relevantChanges,
]);
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
4
169
4
102
4
Previous Highlighted Error
Next Highlighted Error
<?php
namespace Jiminny\Models;
use Carbon\Carbon;
use Database\Factories\ActivityFactory;
use DateTimeInterface;
use Exception;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use InvalidArgumentException;
use Jiminny\Component\ElasticSearch;
use Jiminny\Component\MeetingBot;
use Jiminny\Component\Model\BitwiseFlagTrait;
use Jiminny\Component\PlaybackPage\Comments\Services\ActivityCommentService;
use Jiminny\Component\Sidekick\SidekickService;
use Jiminny\Component\Uuid\UuidAwareInterface;
use Jiminny\Component\Workflow;
use Jiminny\Contracts;
use Jiminny\Contracts\Crm\ProspectInterface;
use Jiminny\DTO\ImportCall\Call;
use Jiminny\Events\Activities\ActivityTypeUpdated;
use Jiminny\Events\Activities\ActivityUpdated;
use Jiminny\Events\Activities\ProspectUpdated;
use Jiminny\Events\Activities\StageUpdated;
use Jiminny\Events\Activities\StatusUpdated;
use Jiminny\Events\Activities\TitleUpdated;
use Jiminny\Exceptions\InvalidArgumentException as InvalidArgumentJiminnyException;
use Jiminny\Exceptions\LogicException;
use Jiminny\Exceptions\RuntimeException;
use Jiminny\Models;
use Jiminny\Models\Activity\ActivitySummaryLog;
use Jiminny\Models\Activity\ActivityUploadSetting;
use Jiminny\Models\Activity\AvailabilityNotification;
use Jiminny\Models\Activity\CoachRequest;
use Jiminny\Models\Activity\Comment;
use Jiminny\Models\Activity\Log;
use Jiminny\Models\Activity\Message;
use Jiminny\Models\Activity\Moment;
use Jiminny\Models\Activity\Note;
use Jiminny\Models\Activity\ParticipantSpeech;
use Jiminny\Models\Activity\Play;
use Jiminny\Models\Activity\Question;
use Jiminny\Models\Activity\Share;
use Jiminny\Models\Activity\Snapshot;
use Jiminny\Models\Activity\Stats;
use Jiminny\Models\Activity\SubscriptionSet;
use Jiminny\Models\Activity\TopicTrigger;
use Jiminny\Models\Activity\Transcription;
use Jiminny\Models\Calendar\CalendarEvent;
use Jiminny\Models\Crm\Configuration;
use Jiminny\Models\Crm\FieldData;
use Jiminny\Models\ElasticSearch\ActivityElasticSearchTrait;
use Jiminny\Models\Feature\FeatureEnum;
use Jiminny\Models\Participant\Connection;
use Jiminny\Models\Playlist\Activity as PlaylistActivity;
use Jiminny\Services\Activity\ActivityProviderRegistry;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataByStrategy;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverFactory;
use Jiminny\Services\Activity\Import\DataResolvers\UpdateCrmDataResolverInterface;
use Jiminny\Traits\Enums;
use Jiminny\Traits\RequiresUUID;
use Jiminny\Utils\CurrencyFormatter;
use NumberFormatter;
use function in_array;
/**
* Jiminny\Models\Activity
*
* @property null|int $auto_score filled from ES hydrator, not in DB!
* @property-read Account|null $account
* @property-read CalendarEvent|null $calendarEvent
* @property-read Contact|null $contact
* @property-read Lead|null $lead
* @property-read Opportunity|null $opportunity
* @property-read Stage|null $stage
* @property int $id
* @property mixed|null $uuid
* @property string|null $source
* @property string|null $external_id
* @property string $provider
* @property string|null $location
* @property string|null $telephony_provider_id
* @property int|null $from_participant_id
* @property int|null $to_participant_id
* @property int|null $device_id
* @property string|null $type
* @property int|null $playbook_category_id
* @property int $user_id
* @property int|null $lead_id
* @property int|null $account_id
* @property int|null $contact_id
* @property int|null $opportunity_id
* @property int|null $stage_id
* @property string|null $value
* @property int|null $crm_configuration_id
* @property string|null $crm_provider_id
* @property string|null $language
* @property int|null $transcription_id
* @property int $duration
* @property string $status
* @property int|null $on_air
* @property int|null $calendar_event_id
* @property string $recording_state
* @property bool|null $recording_preference
* @property int $recording_reason_code
* @property int $summary_reminder_sent
* @property \Illuminate\Support\Carbon|null $log_reminder_sent_at
* @property \Illuminate\Support\Carbon|null $organizer_notified_at
* @property bool|null $has_recording_prompt
* @property bool $is_internal
* @property int $is_locked
* @property int $is_recording
* @property bool|null $is_processed
* @property bool $is_private
* @property bool $is_instant_invite
* @property string|null $poster_path
* @property string|null $summary
* @property string|null $title
* @property string|null $description
* @property \Illuminate\Support\Carbon|null $scheduled_start_time
* @property \Illuminate\Support\Carbon|null $scheduled_end_time
* @property \Illuminate\Support\Carbon|null $actual_start_time
* @property \Illuminate\Support\Carbon|null $actual_end_time
* @property int|null $uploaded_by
* @property \Illuminate\Support\Carbon|null $deleted_at
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property string|null $average_score
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $activeParticipants
* @property-read int|null $active_participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRuleTrigger> $activityScorecardRuleTriggers
* @property-read int|null $activity_scorecard_rule_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Scorecard\ActivityScorecardRule> $activityScorecardRules
* @property-read int|null $activity_scorecard_rules_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, AvailabilityNotification> $availabilityNotifications
* @property-read int|null $availability_notifications_count
* @property-read \Jiminny\Models\PlaybookCategory|null $category
* @property-read \Illuminate\Database\Eloquent\Collection<int, CoachRequest> $coachRequests
* @property-read int|null $coach_requests_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $coachingFeedbacks
* @property-read int|null $coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $coachingMessages
* @property-read int|null $coaching_messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $comments
* @property-read int|null $comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Connection> $connections
* @property-read int|null $connections_count
* @property-read Configuration|null $crm
* @property-read \Illuminate\Database\Eloquent\Collection<int, FieldData> $data
* @property-read int|null $data_count
* @property-read \Jiminny\Models\Device|null $device
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $favoritePlaylists
* @property-read int|null $favorite_playlists_count
* @property-read \Jiminny\Models\Participant|null $from
* @property-read string|null $activity_title
* @property-read mixed $comment_count
* @property-read mixed $duration_for_humans
* @property-read string $duration_for_humans_short
* @property-read int $favorite_count
* @property-read mixed $favorites_count
* @property-read mixed $formatted_value
* @property-read string $id_string
* @property-read \Jiminny\Models\Participant|null $organizer
* @property-read mixed $play_count
* @property-read int|null $plays_count
* @property-read ?ProspectInterface $prospect
* @property-read string|null $prospect_name
* @property-read mixed $prospect_type
* @property-read mixed $share_count
* @property-read int|null $shares_count
* @property-read int|null $tracks_with_telephony_count
* @property-read int|null $visible_comments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\CoachingFeedback> $latestCoachingFeedbacks
* @property-read int|null $latest_coaching_feedbacks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Log> $logs
* @property-read int|null $logs_count
* @property-read \Jiminny\Models\Track|null $masterTrack
* @property-read \Illuminate\Database\Eloquent\Collection<int, Message> $messages
* @property-read int|null $messages_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Moment> $moments
* @property-read int|null $moments_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Note> $notes
* @property-read int|null $notes_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\Share> $participantShares
* @property-read int|null $participant_shares_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, ParticipantSpeech> $participantSpeeches
* @property-read int|null $participant_speeches_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant\ParticipantStats> $participantStats
* @property-read int|null $participant_stats_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Participant> $participants
* @property-read int|null $participants_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, PlaylistActivity> $playlistActivities
* @property-read int|null $playlist_activities_count
* @property-read \Kalnoy\Nestedset\Collection<int, \Jiminny\Models\Playlist> $playlists
* @property-read int|null $playlists_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Play> $plays
* @property-read \Illuminate\Database\Eloquent\Collection<int, Question> $questions
* @property-read int|null $questions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Session> $sessions
* @property-read int|null $sessions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, Share> $shares
* @property-read \Illuminate\Database\Eloquent\Collection<int, Snapshot> $snapshots
* @property-read int|null $snapshots_count
* @property-read Stats|null $stats
* @property-read \Jiminny\Models\Participant|null $to
* @property-read \Illuminate\Database\Eloquent\Collection<int, TopicTrigger> $topicTriggers
* @property-read int|null $topic_triggers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracks
* @property-read int|null $tracks_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Jiminny\Models\Track> $tracksWithTelephony
* @property-read Transcription|null $transcription
* @property-read \Jiminny\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection<int, Comment> $visibleComments
*
* @method static \Illuminate\Database\Eloquent\Collection<int, static> all($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity chunkByIdDesc($count, callable $callback, $column = null, $alias = null)
* @method static \Database\Factories\ActivityFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Collection<int, static> get($columns = ['*'])
* @method static \Jiminny\Component\Eloquent\Builder|Activity heldBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity idOrUuId($idOrUuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity newModelQuery()
* @method static \Jiminny\Component\Eloquent\Builder|Activity newQuery()
* @method static Builder|Activity onlyTrashed()
* @method static \Jiminny\Component\Eloquent\Builder|Activity query()
* @method static \Jiminny\Component\Eloquent\Builder|Activity scheduledBetween(\Carbon\Carbon $start, \Carbon\Carbon $end)
* @method static \Jiminny\Component\Eloquent\Builder|Activity inOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity notInOpenDeals()
* @method static \Jiminny\Component\Eloquent\Builder|Activity forTeam(int $teamId)
* @method static \Jiminny\Component\Eloquent\Builder|Activity search(callable $searchQuery, $key = null, $sortByResults = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity uuid(string $uuid, bool $first = true)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAccountId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereActualStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereAverageScore($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCalendarEventId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereContactId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCreatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmConfigurationId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereCrmProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeletedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDescription($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDeviceId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereDuration($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereFromParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereHasRecordingPrompt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInstantInvite($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsInternal($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsLocked($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsPrivate($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsProcessed($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereIsRecording($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLanguage($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLeadId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLocation($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereLogReminderSentAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOnAir($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOpportunityId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereOrganizerNotifiedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePlaybookCategoryId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity wherePosterPath($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereProvider($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingPreference($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingReasonCode($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereRecordingState($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledEndTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereScheduledStartTime($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSource($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereExternalId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStageId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereStatus($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummary($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereSummaryReminderSent($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTelephonyProviderId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTitle($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereToParticipantId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereTranscriptionId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereType($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUpdatedAt($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUploadedBy($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUserId($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereUuid($value)
* @method static \Jiminny\Component\Eloquent\Builder|Activity whereValue($value)
* @method static Builder|Activity withTrashed()
* @method static Builder|Activity withoutTrashed()
*
* @mixin \Eloquent
*/
class Activity extends Model implements
ElasticSearch\Contract\Searchable,
Workflow\Workflow\WorkflowAwareInterface,
Models\Contracts\ActivityContract,
Contracts\Model\ActivityInterface,
UuidAwareInterface
{
use HasFactory;
use Enums;
use SoftDeletes;
use RequiresUUID;
use BitwiseFlagTrait;
use ElasticSearch\Model\Searchable;
use ActivityElasticSearchTrait;
use Workflow\Workflow\WorkflowAware {
transitionTo as traitTransitionTo;
}
public const int FLAG_RECORDING_REASON_DEFAULT = 0;
// Recording Prompted but never started
public const int FLAG_RECORDING_REASON_COMPLIANCE_PROMPT = 1;
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESUMED = 2;
public const int FLAG_RECORDING_REASON_NO_AUDIO = 3;
// Recording Disabled by Organization
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT = 4;
// Recording was restricted to one-side recordings only
public const int FLAG_RECORDING_REASON_COMPLIANCE_RESTRICT_ONE_SIDE = 8;
// Recording was not started because it was internal and team setting disabled that.
public const int FLAG_RECORDING_REASON_TEAM_INTERNAL_DISABLED = 16;
// Recording was not started because it was internal and user setting disabled that.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED = 32;
// Recording was not started because user setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_USER_AUTOMATIC_DISABLED = 64;
// Recording was not started because team setting disabled automatic recording.
public const int FLAG_RECORDING_REASON_TEAM_AUTOMATIC_DISABLED = 128;
// Recording was not started because user has overriden default.
public const int FLAG_RECORDING_REASON_PREFERENCE_OVERRIDE = 256;
// Recording was not started because they don't want internal, and this meeting was not scheduled/imported in time.
public const int FLAG_RECORDING_REASON_USER_INTERNAL_DISABLED_UNSCHEDULED = 512;
// Recording was not started because their team setting does excludes the meeting type.
public const int FLAG_RECORDING_REASON_UNSUPPORTED_TYPE = 1024;
// Recording was not started because the external provider disabled it (or recording is missing etc).
public const int FLAG_RECORDING_REASON_EXTERNALLY_DISABLED = 2048;
// Recording was stopped externally ("exit-meeting" Pusher event)
public const int FLAG_RECORDING_REASON_STOPPED_EXTERNALLY = 384;
// Recording couldn't be started due to Zoom hosting conflict error
public const int FLAG_RECORDING_REASON_HOSTING_CONFLICT = 448;
// meeting.failed event with reason code BOT_DENIED_FROM_LOBBY
public const int FLAG_RECORDING_REASON_MEETING_BOT_DENIED_FROM_LOBBY = 4096;
// meeting.failed event with reason code LOBBY_TIMEOUT
public const int FLAG_RECORDING_REASON_MEETING_BOT_LOBBY_TIMEOUT = 8192;
// meeting.failed event with reason code BOT_KICKED
public const int FLAG_RECORDING_REASON_MEETING_BOT_KICKED = 16384;
// meeting.failed event with reason code UNKNOWN
public const int FLAG_RECORDING_REASON_MEETING_BOT_UNKNOWN = 32768;
public const int FLAG_RECORDING_REASON_CONSENT_DENIED = 65536;
// Invalid meeting (e.g. URL is invalid, or the meeting is not found)
public const int FLAG_RECORDING_REASON_MEETING_BOT_INVALID = 131072;
// The host stopped the recording.
public const int FLAG_RECORDING_REASON_USER_STOPPED = 262144;
// Recording was not started because an alternative vendor disabled it (or overrode it).
public const int FLAG_RECORDING_REASON_VENDOR_OVERRIDE = 1048576;
// Login required meeting.failed code
public const int FLAG_RECORDING_REASON_LOGIN_REQUIRED = 524288;
// Password for meeting was not provided - meeting.failed code
public const int FLAG_RECORDING_REASON_MEETING_PASSWORD_NOT_PROVIDED = 2097152;
// meeting.failed - when the meeting is locked
public const int FLAG_RECORDING_REASON_MEETING_IS_LOCKED = 4194304;
// max recording duration reached
public const int FLAG_RECORDING_REASON_MAX_DURATION_REACHED = 8388608;
// recording size is too small
public const int FLAG_RECORDING_REASON_EMPTY_RECORDING = 16777216;
// meeting.failed - when bot is redirected to sign in page multiple times
public const int FLAG_RECORDING_REASON_MAX_RESTART_COUNT_IS_REACHED = 33554432;
// meeting.failed event with reason code CONNECTION_LOST
public const int FLAG_RECORDING_REASON_MEETING_BOT_CONNECTION_LOST = 67108864;
// recording is corrupted.
public const int FLAG_RECORDING_REASON_MEDIA_FILE_UNSUPPORTED_MIME_TYPE = 134217728;
// meeting ended in lobby
public const int FLAG_RECORDING_REASON_MEETING_ENDED_IN_LOBBY = 268435456;
// meeting not started
public const int FLAG_RECORDING_REASON_REASON_MEETING_NOT_STARTED = 536870912;
// unfinished zoom custom disclaimer
public const int FLAG_RECORDING_REASON_FEATURE_RULE_NOT_FOUND_ERROR = 1073741824;
// recording download failed - server error
public const int FLAG_RECORDING_REASON_SERVER_ERROR = 2147483648;
// recording download failed - client code 404
public const int FLAG_RECORDING_REASON_NOT_FOUND = 2147483649;
// recording download failed - client code 401, 403
public const int FLAG_RECORDING_REASON_ACCESS_DENIED = 2147483650;
// recording download failed - client code 429
public const int FLAG_RECORDING_REASON_TOO_MANY_REQUESTS = 2147483651;
// recording download failed - unknown client error
public const int FLAG_RECORDING_REASON_CLIENT_ERROR = 2147483652;
// recording download failed - unknown error
public const int FLAG_RECORDING_REASON_UNKNOWN_ERROR = 2147483653;
// It has been setup ahead of time through calendar
public const string STATUS_SCHEDULED = 'scheduled';
// It is awaiting audio.
public const string STATUS_PENDING = 'pending';
// Participant(s) dialed in, awaiting organizer.
public const string STATUS_RINGING = 'ringing';
// Call is in progress.
public const string STATUS_IN_PROGRESS = 'in-progress';
// It has ended.
public const string STATUS_COMPLETED = 'completed';
// Cancelled prior to starting.
public const string STATUS_CANCELLED = 'canceled';
public const string STATUS_DUPLICATED = 'duplicated'; // duplicated conference
public const string STATUS_STARTING_SOON = 'starting-soon';
public const string STATUS_BOT_CREATE_SENT = 'bot-create-sent';
public const string STATUS_BOT_INSTANCE_WORKER_ASSIGNED = 'worker-assigned';
public const string STATUS_BOT_INSTANCE_STARTED = 'bot-started';
// When bot instance is waiting in lobby
public const string STATUS_BOT_INSTANCE_WAITING_LOBBY = 'bot-waiting';
public const string STATUS_BUSY = 'busy';
public const string STATUS_NO_ANSWER = 'no-answer';
public const string STATUS_FAILED = 'failed'; // Used by SMS too
// SMS related
public const string STATUS_ACCEPTED = 'accepted';
public const string STATUS_QUEUED = 'queued';
public const string STATUS_SENDING = 'sending';
public const string STATUS_SENT = 'sent';
public const string STATUS_DELIVERED = 'delivered';
public const string STATUS_UNDELIVERED = 'undelivered';
public const string STATUS_RECEIVING = 'receiving';
public const string STATUS_RECEIVED = 'received';
public const string STATUS_RESENT = 'resent';
public const array SMS_STATUSES = [
Activity::STATUS_RECEIVED,
Activity::STATUS_SENT,
Activity::STATUS_DELIVERED,
];
public const array SOFT_PHONE_CONFERENCE_STATUSES = [
Activity::STATUS_IN_PROGRESS,
Activity::STATUS_COMPLETED,
];
// @todo refactor prefix from `TYPE_` to `CHANNEL_`
public const string TYPE_SOFTPHONE = 'softphone';
public const string TYPE_SOFTPHONE_INBOUND = 'softphone-inbound';
public const string TYPE_CONFERENCE = 'conference';
public const string TYPE_SMS_INBOUND = 'sms-inbound';
public const string TYPE_SMS_OUTBOUND = 'sms-outbound';
public const string TYPE_EMAIL_INBOUND = 'email-inbound';
public const string TYPE_EMAIL_OUTBOUND = 'email-outbound';
public const array CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
self::TYPE_SMS_INBOUND,
self::TYPE_SMS_OUTBOUND,
self::TYPE_EMAIL_INBOUND,
self::TYPE_EMAIL_OUTBOUND,
];
public const array PLAYABLE_CHANNELS = [
self::TYPE_SOFTPHONE,
self::TYPE_SOFTPHONE_INBOUND,
self::TYPE_CONFERENCE,
];
// Recording States
public const string RECORDING_OFF = 'off'; // Default state
public const string RECORDING_IN_PROGRESS = 'in-progress';
public const string RECORDING_PAUSED = 'paused';
public const string RECORDING_STOPPED = 'stopped'; // To never be resumed.
public const string RECORDING_RECORDED = 'recorded'; // At least some portion of it was recorded.
public const string RECORDING_FAILED = 'failed'; // Recording was attempted but failed for some reason.
// Live Stream States
public const int ON_AIR_DEFAULT = 0;
public const int ON_AIR_READY = 1;
public const int ON_AIR_PREPARING = 2;
public const int ON_AIR_STREAMING = 3;
public const int ON_AIR_FINISHED = 4;
public const int ON_AIR_NOT_STREAMED = 5;
public const int ON_AIR_ERROR = -1;
public const string SOURCE_GONG = 'gong';
public const string SOURCE_CHORUS = 'chorus';
public const string SOURCE_OUTLOOK = 'outlook';
public const string SOURCE_GOOGLE = 'google';
// Activity Providers
public const string PROVIDER_TWILIO = 'twilio'; // XXX: This is run via the Jiminny Provider.
public const string PROVIDER_OUTREACH = 'outreach';
public const string PROVIDER_ZOOM_BOT = 'zoom-bot';
public const string PROVIDER_SALESLOFT = 'salesloft';
public const string PROVIDER_GOOGLE = 'google';
public const string PROVIDER_AIRCALL = 'aircall';
public const string PROVIDER_JUSTCALL = 'justcall';
public const string PROVIDER_GOOGLE_MEET = 'google-meet';
public const string PROVIDER_GONG = 'gong';
public const string PROVIDER_HUBSPOT = 'hubspot';
public const string PROVIDER_CLOSE = 'close';
public const string PROVIDER_TEAMS = 'ms-teams';
public const string PROVIDER_SALESFORCE = 'salesforce';
public const string PROVIDER_GROOVE = 'groove';
public const string PROVIDER_XANT = 'xant';
public const string PROVIDER_OFFICE = 'office';
public const string PROVIDER_NATTERBOX = 'natterbox';
public const string PROVIDER_RINGCENTRAL = 'ringcentral';
public const string PROVIDER_RINGCENTRAL_VIDEO = 'ringcentral-video';
public const string PROVIDER_GOTOMEETING = 'go-to-meeting';
public const string PROVIDER_DEMODESK = 'demo-desk';
public const string PROVIDER_DIALPAD = 'dialpad';
public const string PROVIDER_ZOOM_PHONE = 'zoom-phone';
public const string PROVIDER_CLOUDCALL = 'cloudcall';
public const string PROVIDER_CLOUDCALL_US = 'cloudcall-us';
public const string PROVIDER_EIGHT_BY_EIGHT = 'eight-by-eight'; // "8x8" UK
public const string PROVIDER_EIGHT_BY_EIGHT_CA = 'eight-by-eight-ca'; // "8x8" Canada
public const string PROVIDER_EIGHT_BY_EIGHT_AP = 'eight-by-eight-ap'; // "8x8" Australia
public const string PROVIDER_EIGHT_BY_EIGHT_US_EAST = 'eight-by-eight-use'; // "8x8" US East
public const string PROVIDER_EIGHT_BY_EIGHT_US_WEST = 'eight-by-eight-usw'; // "8x8" US West
public const string PROVIDER_CONNECT_AND_SELL = 'connect-and-sell';
public const string PROVIDER_CLOUD_TALK = 'cloud-talk';
public const string PROVIDER_AMAZON_CONNECT = 'amazon-connect';
public const string PROVIDER_VONAGE = 'vonage';
public const string PROVIDER_MIGRATOR = 'migrator';
public const string PROVIDER_UPLOADER = 'uploader';
public const string PROVIDER_TALKDESK = 'talkdesk';
public const string PROVIDER_TWILIO_FLEX = 'twilio-flex';
public const string PROVIDER_TWILIO_FLEX_DIRECT = 'twilio-flex-direct';
public const string PROVIDER_TWILIO_VIDEO = 'twilio-video';
public const string PROVIDER_AVAYA = 'avaya';
public const string PROVIDER_TELUS = 'telus';
public const string PROVIDER_FIVE_NINE = 'five-nine';
public const string PROVIDER_APOLLO = 'apollo';
public const string PROVIDER_ORUM = 'orum';
public const string PROVIDER_BLOOBIRDS = 'bloobirds';
/**
* @const API_PROVIDERS
* A list of integrations that import calls via API instead of webhooks
*/
public const array API_PROVIDERS = [
self::PROVIDER_OUTREACH,
self::PROVIDER_SALESLOFT,
self::PROVIDER_HUBSPOT,
self::PROVIDER_GROOVE,
self::PROVIDER_XANT,
self::PROVIDER_NATTERBOX,
self::PROVIDER_CLOUDCALL,
self::PROVIDER_CLOUDCALL_US,
self::PROVIDER_EIGHT_BY_EIGHT,
self::PROVIDER_EIGHT_BY_EIGHT_CA,
self::PROVIDER_EIGHT_BY_EIGHT_AP,
self::PROVIDER_EIGHT_BY_EIGHT_US_EAST,
self::PROVIDER_EIGHT_BY_EIGHT_US_WEST,
self::PROVIDER_CONNECT_AND_SELL,
self::PROVIDER_CLOUD_TALK,
self::PROVIDER_AMAZON_CONNECT,
self::PROVIDER_VONAGE,
self::PROVIDER_TALKDESK,
self::PROVIDER_TWILIO_VIDEO,
self::PROVIDER_TWILIO_FLEX,
self::PROVIDER_TWILIO_FLEX_DIRECT,
self::PROVIDER_FIVE_NINE,
self::PROVIDER_APOLLO,
self::PROVIDER_ORUM,
self::PROVIDER_BLOOBIRDS,
self::PROVIDER_RINGCENTRAL,
self::PROVIDER_AVAYA,
self::PROVIDER_TELUS,
];
public const array FINITE_STATES = [
self::TYPE_SOFTPHONE => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_SOFTPHONE_INBOUND => [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_NO_ANSWER,
self::STATUS_BUSY,
],
self::TYPE_CONFERENCE => self::FINITE_STATES_CONFERENCE,
];
public const array FINITE_STATES_CONFERENCE = [
self::STATUS_COMPLETED,
self::STATUS_FAILED,
self::STATUS_CANCELLED,
];
public const array MEETING_BOT_JOIN_ATTEMPTED = [
self::STATUS_BOT_INSTANCE_WAITING_LOBBY,
self::STATUS_BOT_INSTANCE_STARTED,
];
public static array $enumStatuses = [
self::STATUS_SCHEDULED,
self::STATUS_PENDING,
self::STATUS_RINGING,
self::STATUS_IN_PROGRESS,
self::STATUS_COMPLETED,
self::STATUS_CANCELLED,
self::STATUS_BUSY,
self::STATUS_NO_ANSWER,
self::STATUS_FAILED,
self::STATUS_ACCEPTED,
self::STATUS_QUEUED,
self::STATUS_SENDING,
...
|
43031
|
NULL
|
NULL
|
NULL
|